bulltrackers-module 1.0.263 → 1.0.265
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/functions/computation-system/WorkflowOrchestrator.js +58 -22
- package/functions/computation-system/context/ManifestBuilder.js +37 -9
- package/functions/computation-system/executors/StandardExecutor.js +42 -7
- package/functions/computation-system/layers/profiling.js +309 -149
- package/functions/computation-system/persistence/FirestoreUtils.js +2 -10
- package/functions/computation-system/persistence/ResultCommitter.js +106 -199
- package/functions/computation-system/persistence/StatusRepository.js +16 -5
- package/functions/computation-system/tools/BuildReporter.js +9 -19
- package/functions/root-data-indexer/index.js +34 -63
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Profiling Layer - Intelligence Engine (
|
|
2
|
+
* @fileoverview Profiling Layer - Intelligence Engine (V6)
|
|
3
3
|
* Encapsulates advanced behavioral profiling, psychological scoring, and classification schemas.
|
|
4
|
+
* UPDATED: Added SmartMoneyScorer for advanced multi-factor user classification.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const SCHEMAS = {
|
|
@@ -21,118 +22,332 @@ const SCHEMAS = {
|
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
// ========================================================================
|
|
24
|
-
// 1.
|
|
25
|
+
// 1. SMART MONEY SCORING ENGINE (NEW)
|
|
25
26
|
// ========================================================================
|
|
26
27
|
|
|
27
|
-
class
|
|
28
|
+
class SmartMoneyScorer {
|
|
29
|
+
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
30
|
-
* Checks if the user holds "dead money" positions that are hovering near breakeven
|
|
31
|
-
* for extended periods, refusing to close them.
|
|
32
|
-
* @param {Array} openPositions - Current holdings. Needs OpenDateTime (Speculators).
|
|
33
|
-
* @param {number} thresholdPct - +/- % range around 0 PnL (e.g. 2%).
|
|
34
|
-
* @param {number} minDaysHeld - Minimum days held to qualify as "Anchored".
|
|
31
|
+
* Internal Helper: Calculate Pearson Correlation
|
|
35
32
|
*/
|
|
36
|
-
static
|
|
37
|
-
if (!
|
|
33
|
+
static _correlation(x, y) {
|
|
34
|
+
if (!x || !y || x.length !== y.length || x.length < 2) return 0;
|
|
35
|
+
const n = x.length;
|
|
36
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
sumX += x[i]; sumY += y[i];
|
|
39
|
+
sumXY += x[i] * y[i];
|
|
40
|
+
sumX2 += x[i] * x[i]; sumY2 += y[i] * y[i];
|
|
41
|
+
}
|
|
42
|
+
const numerator = (n * sumXY) - (sumX * sumY);
|
|
43
|
+
const denominator = Math.sqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
|
|
44
|
+
return (denominator === 0) ? 0 : numerator / denominator;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mode 1: Portfolio-Based Scoring
|
|
49
|
+
* Heuristics:
|
|
50
|
+
* 1. Diversification (Sector/Asset count)
|
|
51
|
+
* 2. Allocation Efficiency (Correlation of Size vs Profit)
|
|
52
|
+
* 3. Shorting Competence
|
|
53
|
+
* 4. Concentration Risk (HHI)
|
|
54
|
+
*/
|
|
55
|
+
static scorePortfolio(portfolio, userType, prices, mappings, math) {
|
|
56
|
+
const positions = math.extract.getPositions(portfolio, userType);
|
|
57
|
+
if (!positions || positions.length === 0) return { score: 0, label: SCHEMAS.LABELS.NEUTRAL };
|
|
58
|
+
|
|
59
|
+
let totalInvested = 0;
|
|
60
|
+
let weightedPnL = 0;
|
|
61
|
+
let shortInvested = 0;
|
|
62
|
+
let shortPnL = 0;
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
64
|
+
const weights = [];
|
|
65
|
+
const pnls = [];
|
|
66
|
+
const sectors = new Set();
|
|
67
|
+
const tickers = new Set();
|
|
43
68
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
// 1. Data Aggregation
|
|
70
|
+
for (const pos of positions) {
|
|
71
|
+
const invested = math.extract.getPositionWeight(pos, userType);
|
|
72
|
+
const pnl = math.extract.getNetProfit(pos); // %
|
|
73
|
+
const instId = math.extract.getInstrumentId(pos);
|
|
74
|
+
const isShort = math.extract.getDirection(pos) === 'Sell';
|
|
75
|
+
|
|
76
|
+
const sector = mappings.instrumentToSector[instId];
|
|
77
|
+
const ticker = mappings.instrumentToTicker[instId];
|
|
78
|
+
|
|
79
|
+
if (invested > 0) {
|
|
80
|
+
totalInvested += invested;
|
|
81
|
+
weightedPnL += (pnl * invested);
|
|
82
|
+
weights.push(invested);
|
|
83
|
+
pnls.push(pnl);
|
|
49
84
|
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
85
|
+
if (sector) sectors.add(sector);
|
|
86
|
+
if (ticker) tickers.add(ticker);
|
|
87
|
+
|
|
88
|
+
if (isShort) {
|
|
89
|
+
shortInvested += invested;
|
|
90
|
+
shortPnL += (pnl * invested);
|
|
53
91
|
}
|
|
54
92
|
}
|
|
55
93
|
}
|
|
94
|
+
|
|
95
|
+
if (totalInvested === 0) return { score: 0, label: SCHEMAS.LABELS.NEUTRAL };
|
|
96
|
+
|
|
97
|
+
// 2. Metrics Calculation
|
|
98
|
+
const avgPnL = weightedPnL / totalInvested;
|
|
56
99
|
|
|
57
|
-
|
|
100
|
+
// A. Allocation Efficiency (Do they bet big on winners?)
|
|
101
|
+
// Correlation between Invested Amount and PnL %
|
|
102
|
+
const allocEfficiency = this._correlation(weights, pnls); // -1 to 1
|
|
103
|
+
|
|
104
|
+
// B. Diversification & Concentration (HHI)
|
|
105
|
+
// Sum of squared market shares. 1.0 = Monopoly. 0.0 = Infinite.
|
|
106
|
+
let hhi = 0;
|
|
107
|
+
for (const w of weights) {
|
|
108
|
+
const share = w / totalInvested;
|
|
109
|
+
hhi += (share * share);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// C. Shorting Competence
|
|
113
|
+
const shortRatio = shortInvested / totalInvested;
|
|
114
|
+
const avgShortPnL = shortInvested > 0 ? shortPnL / shortInvested : 0;
|
|
115
|
+
|
|
116
|
+
// 3. Scoring Logic
|
|
117
|
+
let score = 50;
|
|
118
|
+
|
|
119
|
+
// Efficiency Bonus: If > 0.5, they size winners up. (+20)
|
|
120
|
+
// If < -0.3, they are bagholding losers with large size (-15)
|
|
121
|
+
if (allocEfficiency > 0.5) score += 20;
|
|
122
|
+
else if (allocEfficiency < -0.3) score -= 15;
|
|
123
|
+
|
|
124
|
+
// Profitability (The ultimate metric)
|
|
125
|
+
if (avgPnL > 5) score += 10;
|
|
126
|
+
if (avgPnL > 20) score += 10;
|
|
127
|
+
if (avgPnL < -10) score -= 10;
|
|
128
|
+
if (avgPnL < -25) score -= 15;
|
|
129
|
+
|
|
130
|
+
// Concentration Logic
|
|
131
|
+
// High Concentration (HHI > 0.3) is "Smart" ONLY if profitable (Sniper)
|
|
132
|
+
// High Concentration and unprofitable is "Dumb" (Bagholder/Gambler)
|
|
133
|
+
if (hhi > 0.3) {
|
|
134
|
+
if (avgPnL > 5) score += 10; // Sniper
|
|
135
|
+
else if (avgPnL < -5) score -= 10; // Reckless
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Diversification Logic
|
|
139
|
+
// High Sector count (>3) reduces risk penalty
|
|
140
|
+
if (sectors.size >= 4) score += 5;
|
|
141
|
+
|
|
142
|
+
// Shorting Logic
|
|
143
|
+
// Penalize speculation unless they are actually good at it
|
|
144
|
+
if (shortRatio > 0.1) {
|
|
145
|
+
if (avgShortPnL > 0) score += 10; // Smart Short
|
|
146
|
+
else score -= 10; // Failed Speculation
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
score: Math.max(0, Math.min(100, score)),
|
|
151
|
+
metrics: { allocEfficiency, hhi, avgPnL, shortRatio, sectorCount: sectors.size }
|
|
152
|
+
};
|
|
58
153
|
}
|
|
59
154
|
|
|
60
155
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
156
|
+
* Mode 2: History-Based Scoring
|
|
157
|
+
* Heuristics:
|
|
158
|
+
* 1. Win/Loss Ratio & Profit Factor
|
|
159
|
+
* 2. Asset Consistency (Revenge trading vs Specialist)
|
|
160
|
+
* 3. Entry Efficiency (Buying Lows)
|
|
161
|
+
* 4. Exit Efficiency (Selling Highs - Opportunity Cost)
|
|
162
|
+
* 5. Churn (Overtrading)
|
|
163
|
+
* 6. DCA/Entry Patterns
|
|
64
164
|
*/
|
|
65
|
-
static
|
|
66
|
-
|
|
67
|
-
|
|
165
|
+
static scoreHistory(historyDoc, prices, mappings, math) {
|
|
166
|
+
// Handle V2 Schema (PublicHistoryPositions)
|
|
167
|
+
const trades = historyDoc?.PublicHistoryPositions || [];
|
|
168
|
+
// Handle V1 Schema fallback if needed (though prompt implies V2)
|
|
169
|
+
|
|
170
|
+
if (trades.length < 5) return { score: 0, label: SCHEMAS.LABELS.NEUTRAL };
|
|
68
171
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
172
|
+
// Filter valid trades
|
|
173
|
+
const validTrades = trades.filter(t => t.OpenDateTime && t.CloseDateTime && t.InstrumentID);
|
|
174
|
+
if (validTrades.length < 5) return { score: 0, label: SCHEMAS.LABELS.NEUTRAL };
|
|
175
|
+
|
|
176
|
+
let wins = 0, losses = 0;
|
|
177
|
+
let totalWinPct = 0, totalLossPct = 0;
|
|
178
|
+
let entryScores = [];
|
|
179
|
+
const assetsTraded = new Map(); // ID -> { count, pnl }
|
|
180
|
+
|
|
181
|
+
// Time sorting for Churn analysis
|
|
182
|
+
validTrades.sort((a, b) => new Date(a.OpenDateTime) - new Date(b.OpenDateTime));
|
|
183
|
+
const firstDate = new Date(validTrades[0].OpenDateTime);
|
|
184
|
+
const lastDate = new Date(validTrades[validTrades.length-1].OpenDateTime);
|
|
185
|
+
const daysActive = Math.max(1, (lastDate - firstDate) / 86400000);
|
|
186
|
+
|
|
187
|
+
for (const t of validTrades) {
|
|
188
|
+
const ticker = mappings.instrumentToTicker[t.InstrumentID];
|
|
73
189
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
190
|
+
// Asset Consistency
|
|
191
|
+
if (!assetsTraded.has(t.InstrumentID)) assetsTraded.set(t.InstrumentID, { count: 0, pnl: 0 });
|
|
192
|
+
const assetStat = assetsTraded.get(t.InstrumentID);
|
|
193
|
+
assetStat.count++;
|
|
194
|
+
assetStat.pnl += t.NetProfit;
|
|
195
|
+
|
|
196
|
+
// A. Win/Loss Stats
|
|
197
|
+
if (t.NetProfit > 0) { wins++; totalWinPct += t.NetProfit; }
|
|
198
|
+
else { losses++; totalLossPct += Math.abs(t.NetProfit); }
|
|
199
|
+
|
|
200
|
+
// B. Entry Timing (Requires Price History)
|
|
201
|
+
if (ticker && prices) {
|
|
202
|
+
const priceHist = math.priceExtractor.getHistory(prices, ticker);
|
|
203
|
+
if (priceHist && priceHist.length > 0) {
|
|
204
|
+
// 1.0 = Perfect Low, 0.0 = Bought High
|
|
205
|
+
const eff = ExecutionAnalytics.calculateEfficiency(t.OpenRate, priceHist, t.OpenDateTime, t.IsBuy ? 'Buy' : 'Sell');
|
|
206
|
+
entryScores.push(eff);
|
|
207
|
+
}
|
|
80
208
|
}
|
|
81
209
|
}
|
|
82
210
|
|
|
83
|
-
const
|
|
84
|
-
const
|
|
211
|
+
const avgWin = wins > 0 ? totalWinPct / wins : 0;
|
|
212
|
+
const avgLoss = losses > 0 ? totalLossPct / losses : 1;
|
|
213
|
+
const profitFactor = (wins * avgWin) / Math.max(1, (losses * avgLoss));
|
|
214
|
+
|
|
215
|
+
// C. Entry Skill
|
|
216
|
+
const avgEntrySkill = entryScores.length > 0 ? math.compute.average(entryScores) : 0.5;
|
|
85
217
|
|
|
86
|
-
|
|
87
|
-
|
|
218
|
+
// D. Consistency / Specialization
|
|
219
|
+
// Do they trade 100 tickers once (Gambler) or 5 tickers 20 times (Specialist)?
|
|
220
|
+
const totalTrades = validTrades.length;
|
|
221
|
+
const uniqueAssets = assetsTraded.size;
|
|
222
|
+
const specializationRatio = 1 - (uniqueAssets / totalTrades); // Higher = More specialized
|
|
223
|
+
|
|
224
|
+
// E. Overtrading (Churn)
|
|
225
|
+
const tradesPerDay = totalTrades / daysActive;
|
|
226
|
+
|
|
227
|
+
// F. Revenge Trading Check
|
|
228
|
+
// High count on a specific asset with negative total PnL
|
|
229
|
+
let revengeScore = 0;
|
|
230
|
+
for (const [id, stat] of assetsTraded.entries()) {
|
|
231
|
+
if (stat.pnl < -20 && stat.count > 5) revengeScore += 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Scoring Logic
|
|
235
|
+
let score = 50;
|
|
236
|
+
|
|
237
|
+
// Profit Factor (Primary Driver)
|
|
238
|
+
if (profitFactor > 1.2) score += 10;
|
|
239
|
+
if (profitFactor > 2.0) score += 15;
|
|
240
|
+
if (profitFactor < 0.8) score -= 15;
|
|
241
|
+
|
|
242
|
+
// Entry Efficiency
|
|
243
|
+
if (avgEntrySkill > 0.7) score += 10; // Sniper
|
|
244
|
+
if (avgEntrySkill < 0.3) score -= 10; // FOMO
|
|
245
|
+
|
|
246
|
+
// Specialization
|
|
247
|
+
if (specializationRatio > 0.6) score += 5; // Specialist bonus
|
|
248
|
+
if (specializationRatio < 0.1 && totalTrades > 20) score -= 5; // Scattergun penalty
|
|
249
|
+
|
|
250
|
+
// Churn Penalty
|
|
251
|
+
if (tradesPerDay > 10 && profitFactor < 1.0) score -= 10; // Brokerage Cash Cow
|
|
252
|
+
|
|
253
|
+
// Revenge Penalty
|
|
254
|
+
if (revengeScore > 0) score -= (revengeScore * 5);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
score: Math.max(0, Math.min(100, score)),
|
|
258
|
+
metrics: { profitFactor, avgEntrySkill, specializationRatio, tradesPerDay, revengeScore }
|
|
259
|
+
};
|
|
88
260
|
}
|
|
89
261
|
|
|
90
262
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* Losses hurt approx 2.25x more than gains feel good.
|
|
94
|
-
* @param {number} pnl - Net Profit %.
|
|
263
|
+
* Mode 3: Hybrid Scoring
|
|
264
|
+
* Merges Portfolio (Unrealized/Current) and History (Realized/Past).
|
|
95
265
|
*/
|
|
96
|
-
static
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
266
|
+
static scoreHybrid(context) {
|
|
267
|
+
const { user, prices, mappings, math } = context;
|
|
268
|
+
|
|
269
|
+
// Get Sub-Scores
|
|
270
|
+
const pScore = this.scorePortfolio(user.portfolio.today, user.type, prices, mappings, math);
|
|
271
|
+
const hScore = this.scoreHistory(user.history.today, prices, mappings, math);
|
|
272
|
+
|
|
273
|
+
let finalScore = 50;
|
|
274
|
+
let method = 'Neutral';
|
|
275
|
+
|
|
276
|
+
const hasHistory = hScore && hScore.score > 0;
|
|
277
|
+
const hasPortfolio = pScore && pScore.score > 0;
|
|
278
|
+
|
|
279
|
+
if (hasHistory && hasPortfolio) {
|
|
280
|
+
// Weighted: 60% Track Record (History), 40% Current Positioning (Portfolio)
|
|
281
|
+
finalScore = (hScore.score * 0.6) + (pScore.score * 0.4);
|
|
282
|
+
method = 'Hybrid';
|
|
283
|
+
} else if (hasHistory) {
|
|
284
|
+
finalScore = hScore.score;
|
|
285
|
+
method = 'HistoryOnly';
|
|
286
|
+
} else if (hasPortfolio) {
|
|
287
|
+
finalScore = pScore.score;
|
|
288
|
+
method = 'PortfolioOnly';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Classification Label
|
|
292
|
+
let label = SCHEMAS.LABELS.NEUTRAL;
|
|
293
|
+
if (finalScore >= 80) label = SCHEMAS.LABELS.ELITE;
|
|
294
|
+
else if (finalScore >= 65) label = SCHEMAS.LABELS.SMART;
|
|
295
|
+
else if (finalScore <= 35) label = SCHEMAS.LABELS.GAMBLER;
|
|
296
|
+
else if (finalScore <= 50) label = SCHEMAS.LABELS.DUMB;
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
totalScore: Math.round(finalScore),
|
|
300
|
+
label: label,
|
|
301
|
+
method: method,
|
|
302
|
+
components: {
|
|
303
|
+
portfolio: pScore,
|
|
304
|
+
history: hScore
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ========================================================================
|
|
311
|
+
// 2. SUPPORTING ANALYTICS ENGINES
|
|
312
|
+
// ========================================================================
|
|
313
|
+
|
|
314
|
+
class CognitiveBiases {
|
|
315
|
+
static calculateAnchoringScore(openPositions, thresholdPct = 2.0, minDaysHeld = 14) {
|
|
316
|
+
if (!openPositions || openPositions.length === 0) return 0;
|
|
317
|
+
let anchoredCount = 0, validPositions = 0;
|
|
318
|
+
const now = Date.now(), msPerDay = 86400000;
|
|
319
|
+
for (const pos of openPositions) {
|
|
320
|
+
if (pos.OpenDateTime) {
|
|
321
|
+
validPositions++;
|
|
322
|
+
const ageDays = (now - new Date(pos.OpenDateTime).getTime()) / msPerDay;
|
|
323
|
+
if (ageDays > minDaysHeld && Math.abs(pos.NetProfit) < thresholdPct) { anchoredCount++; }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return validPositions > 0 ? (anchoredCount / validPositions) : 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static calculateDispositionEffect(historyTrades) {
|
|
330
|
+
let winDur = 0, winCount = 0, lossDur = 0, lossCount = 0;
|
|
331
|
+
for (const t of historyTrades) {
|
|
332
|
+
if (!t.OpenDateTime || !t.CloseDateTime) continue;
|
|
333
|
+
const dur = (new Date(t.CloseDateTime) - new Date(t.OpenDateTime)) / 3600000;
|
|
334
|
+
if (t.NetProfit > 0) { winDur += dur; winCount++; } else if (t.NetProfit < 0) { lossDur += dur; lossCount++; }
|
|
101
335
|
}
|
|
336
|
+
const avgWinHold = winCount > 0 ? winDur / winCount : 0;
|
|
337
|
+
const avgLossHold = lossCount > 0 ? lossDur / lossCount : 0;
|
|
338
|
+
if (avgWinHold === 0) return 2.0;
|
|
339
|
+
return avgLossHold / avgWinHold;
|
|
102
340
|
}
|
|
103
341
|
}
|
|
104
342
|
|
|
105
343
|
class SkillAttribution {
|
|
106
|
-
/**
|
|
107
|
-
* Calculates Selection Skill (Alpha) by comparing User PnL vs Asset Benchmark.
|
|
108
|
-
* Note: Since we don't have individual asset performance histories easily available
|
|
109
|
-
* in the user context, we use the 'Insights' global growth as a daily benchmark proxy.
|
|
110
|
-
* @param {Array} userPositions - Current open positions.
|
|
111
|
-
* @param {Object} dailyInsights - Map of InstrumentID -> Insight Data (which contains 'growth').
|
|
112
|
-
*/
|
|
113
344
|
static calculateSelectionAlpha(userPositions, dailyInsights) {
|
|
114
|
-
let totalAlpha = 0;
|
|
115
|
-
let count = 0;
|
|
116
|
-
|
|
345
|
+
let totalAlpha = 0, count = 0;
|
|
117
346
|
for (const pos of userPositions) {
|
|
118
347
|
const instrumentId = pos.InstrumentID;
|
|
119
|
-
|
|
120
|
-
// If passed as array, we find the item.
|
|
121
|
-
let insight = null;
|
|
122
|
-
if (Array.isArray(dailyInsights)) {
|
|
123
|
-
insight = dailyInsights.find(i => i.instrumentId === instrumentId);
|
|
124
|
-
}
|
|
125
|
-
|
|
348
|
+
let insight = Array.isArray(dailyInsights) ? dailyInsights.find(i => i.instrumentId === instrumentId) : null;
|
|
126
349
|
if (insight && typeof insight.growth === 'number') {
|
|
127
|
-
|
|
128
|
-
// We use NetProfit as a proxy for "Performance" state.
|
|
129
|
-
// A Better proxy: Is their NetProfit > The Asset's Weekly Growth?
|
|
130
|
-
// This is a rough heuristic given schema limitations.
|
|
131
|
-
|
|
132
|
-
// If the user is long and PnL > 0, and Growth is negative, that's high alpha (Bucking the trend).
|
|
133
|
-
// Simplified: Just returning the difference.
|
|
134
|
-
const diff = pos.NetProfit - insight.growth;
|
|
135
|
-
totalAlpha += diff;
|
|
350
|
+
totalAlpha += (pos.NetProfit - insight.growth);
|
|
136
351
|
count++;
|
|
137
352
|
}
|
|
138
353
|
}
|
|
@@ -198,90 +413,35 @@ class AdaptiveAnalytics {
|
|
|
198
413
|
}
|
|
199
414
|
}
|
|
200
415
|
|
|
416
|
+
// Legacy Wrapper for backward compatibility with older calculations
|
|
201
417
|
class UserClassifier {
|
|
202
418
|
static classify(context) {
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const validHistory = history.filter(t => t.OpenDateTime);
|
|
206
|
-
validHistory.sort((a, b) => new Date(a.OpenDateTime) - new Date(b.OpenDateTime));
|
|
207
|
-
const portfolio = math.extract.getPositions(user.portfolio.today, user.type);
|
|
208
|
-
const summary = math.history.getSummary(user.history.today);
|
|
209
|
-
if (!summary) return { intelligence: { label: SCHEMAS.LABELS.NEUTRAL, score: 0 }, style: { primary: SCHEMAS.STYLES.INVESTOR } };
|
|
210
|
-
|
|
211
|
-
let entryScores = [];
|
|
212
|
-
const recentTrades = validHistory.slice(-20);
|
|
213
|
-
for (const t of recentTrades) {
|
|
214
|
-
const ticker = context.mappings.instrumentToTicker[t.InstrumentID];
|
|
215
|
-
const priceData = math.priceExtractor.getHistory(prices, ticker);
|
|
216
|
-
if (priceData && priceData.length > 0) { entryScores.push(ExecutionAnalytics.calculateEfficiency(t.OpenRate, priceData, t.OpenDateTime, 'Buy')); }
|
|
217
|
-
}
|
|
218
|
-
const avgEntryEff = math.compute.average(entryScores) || 0.5;
|
|
219
|
-
const dispositionSkew = Psychometrics.computeDispositionSkew(validHistory, portfolio);
|
|
220
|
-
const revengeScore = Psychometrics.detectRevengeTrading(validHistory);
|
|
221
|
-
const adaptationScore = AdaptiveAnalytics.analyzeDrawdownAdaptation(validHistory);
|
|
222
|
-
|
|
223
|
-
// New Cognitive Bias Checks
|
|
224
|
-
const anchoring = CognitiveBiases.calculateAnchoringScore(portfolio);
|
|
225
|
-
const dispositionTime = CognitiveBiases.calculateDispositionEffect(validHistory);
|
|
226
|
-
|
|
227
|
-
const riskAdjustedReturn = summary.avgLossPct === 0 ? 10 : (summary.avgProfitPct / Math.abs(summary.avgLossPct));
|
|
228
|
-
let smartScore = 50;
|
|
229
|
-
if (riskAdjustedReturn > 1.5) smartScore += 10;
|
|
230
|
-
if (riskAdjustedReturn > 3.0) smartScore += 10;
|
|
231
|
-
if (summary.winRatio > 60) smartScore += 10;
|
|
232
|
-
if (avgEntryEff > 0.7) smartScore += 10;
|
|
233
|
-
if (avgEntryEff < 0.3) smartScore -= 5;
|
|
234
|
-
if (dispositionSkew > 15) smartScore -= 20; else if (dispositionSkew < 5) smartScore += 10;
|
|
235
|
-
if (revengeScore > 0.3) smartScore -= 25;
|
|
236
|
-
if (adaptationScore > 0.5) smartScore += 5; if (adaptationScore < -0.5) smartScore -= 10;
|
|
419
|
+
// Delegate to the new robust Hybrid Scorer
|
|
420
|
+
const result = SmartMoneyScorer.scoreHybrid(context);
|
|
237
421
|
|
|
238
|
-
//
|
|
239
|
-
if (anchoring > 0.3) smartScore -= 10;
|
|
240
|
-
if (dispositionTime > 1.5) smartScore -= 10;
|
|
241
|
-
|
|
242
|
-
let label = SCHEMAS.LABELS.NEUTRAL;
|
|
243
|
-
if (smartScore >= 80) label = SCHEMAS.LABELS.ELITE;
|
|
244
|
-
else if (smartScore >= 65) label = SCHEMAS.LABELS.SMART;
|
|
245
|
-
else if (smartScore <= 30) label = SCHEMAS.LABELS.GAMBLER;
|
|
246
|
-
else if (smartScore <= 45) label = SCHEMAS.LABELS.DUMB;
|
|
247
|
-
|
|
248
|
-
const styleProfile = this.classifyStyle(validHistory, portfolio);
|
|
422
|
+
// Map new result structure to legacy structure expected by V1 calcs
|
|
249
423
|
return {
|
|
250
|
-
intelligence: {
|
|
251
|
-
|
|
424
|
+
intelligence: {
|
|
425
|
+
label: result.label,
|
|
426
|
+
score: result.totalScore,
|
|
427
|
+
isSmart: result.totalScore >= 65
|
|
428
|
+
},
|
|
429
|
+
style: { primary: SCHEMAS.STYLES.INVESTOR }, // Placeholder
|
|
252
430
|
metrics: {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
revengeTendency: revengeScore,
|
|
256
|
-
riskRewardRatio: riskAdjustedReturn,
|
|
257
|
-
drawdownAdaptation: adaptationScore,
|
|
258
|
-
biasAnchoring: anchoring,
|
|
259
|
-
biasDispositionTime: dispositionTime
|
|
431
|
+
profitFactor: result.components.history?.metrics?.profitFactor || 0,
|
|
432
|
+
allocEfficiency: result.components.portfolio?.metrics?.allocEfficiency || 0
|
|
260
433
|
}
|
|
261
434
|
};
|
|
262
435
|
}
|
|
263
|
-
|
|
264
|
-
static classifyStyle(history, portfolio) {
|
|
265
|
-
let totalMinutes = 0; let validTrades = 0;
|
|
266
|
-
history.forEach(t => { if (t.OpenDateTime && t.CloseDateTime) { const open = new Date(t.OpenDateTime); const close = new Date(t.CloseDateTime); totalMinutes += (close - open) / 60000; validTrades++; } });
|
|
267
|
-
const avgHoldTime = validTrades > 0 ? totalMinutes / validTrades : 0;
|
|
268
|
-
let baseStyle = SCHEMAS.STYLES.INVESTOR;
|
|
269
|
-
if (validTrades > 0) { if (avgHoldTime < 60) baseStyle = SCHEMAS.STYLES.SCALPER; else if (avgHoldTime < 60 * 24) baseStyle = SCHEMAS.STYLES.DAY_TRADER; else if (avgHoldTime < 60 * 24 * 7) baseStyle = SCHEMAS.STYLES.SWING_TRADER; }
|
|
270
|
-
const subStyles = new Set();
|
|
271
|
-
const assets = [...history, ...portfolio]; let leverageCount = 0;
|
|
272
|
-
assets.forEach(p => { if ((p.Leverage || 1) > 1) leverageCount++; });
|
|
273
|
-
const tradeCount = assets.length || 1;
|
|
274
|
-
if ((leverageCount / tradeCount) > 0.3) subStyles.add("Speculative"); if ((leverageCount / tradeCount) > 0.8) subStyles.add("High-Leverage");
|
|
275
|
-
return { primary: baseStyle, tags: Array.from(subStyles), avgHoldTimeMinutes: avgHoldTime };
|
|
276
|
-
}
|
|
277
436
|
}
|
|
278
437
|
|
|
279
438
|
module.exports = {
|
|
280
439
|
SCHEMAS,
|
|
281
440
|
UserClassifier,
|
|
441
|
+
SmartMoneyScorer, // <-- Exporting the new engine
|
|
282
442
|
ExecutionAnalytics,
|
|
283
443
|
Psychometrics,
|
|
284
444
|
AdaptiveAnalytics,
|
|
285
|
-
CognitiveBiases,
|
|
286
|
-
SkillAttribution
|
|
445
|
+
CognitiveBiases,
|
|
446
|
+
SkillAttribution
|
|
287
447
|
};
|
|
@@ -42,18 +42,10 @@ async function commitBatchInChunks(config, deps, writes, operationName) {
|
|
|
42
42
|
for (const write of writes) {
|
|
43
43
|
let docSize = 100;
|
|
44
44
|
try { if (write.data) docSize = JSON.stringify(write.data).length; } catch (e) { }
|
|
45
|
-
|
|
46
|
-
if (docSize >
|
|
47
|
-
logger.log('WARN', `[${operationName}] Large document detected (~${(docSize / 1024).toFixed(2)} KB).`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if ((currentOpsCount + 1 > MAX_BATCH_OPS) || (currentBytesEst + docSize > MAX_BATCH_BYTES)) {
|
|
51
|
-
await commitAndReset();
|
|
52
|
-
}
|
|
53
|
-
|
|
45
|
+
if (docSize > 900 * 1024) { logger.log('WARN', `[${operationName}] Large document detected (~${(docSize / 1024).toFixed(2)} KB).`); }
|
|
46
|
+
if ((currentOpsCount + 1 > MAX_BATCH_OPS) || (currentBytesEst + docSize > MAX_BATCH_BYTES)) { await commitAndReset(); }
|
|
54
47
|
const options = write.options || { merge: true };
|
|
55
48
|
currentBatch.set(write.ref, write.data, options);
|
|
56
|
-
|
|
57
49
|
currentOpsCount++;
|
|
58
50
|
currentBytesEst += docSize;
|
|
59
51
|
}
|