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.
@@ -1,6 +1,7 @@
1
1
  /**
2
- * @fileoverview Profiling Layer - Intelligence Engine (V5)
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. BEHAVIORAL ANALYTICS ENGINES
25
+ // 1. SMART MONEY SCORING ENGINE (NEW)
25
26
  // ========================================================================
26
27
 
27
- class CognitiveBiases {
28
+ class SmartMoneyScorer {
29
+
28
30
  /**
29
- * Anchoring Bias Detector.
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 calculateAnchoringScore(openPositions, thresholdPct = 2.0, minDaysHeld = 14) {
37
- if (!openPositions || openPositions.length === 0) return 0;
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
- let anchoredCount = 0;
40
- let validPositions = 0;
41
- const now = Date.now();
42
- const msPerDay = 86400000;
64
+ const weights = [];
65
+ const pnls = [];
66
+ const sectors = new Set();
67
+ const tickers = new Set();
43
68
 
44
- for (const pos of openPositions) {
45
- // Only applicable if we have OpenDateTime (Speculator Schema)
46
- if (pos.OpenDateTime) {
47
- validPositions++;
48
- const ageDays = (now - new Date(pos.OpenDateTime).getTime()) / msPerDay;
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
- // Is the trade old AND hovering near 0% PnL?
51
- if (ageDays > minDaysHeld && Math.abs(pos.NetProfit) < thresholdPct) {
52
- anchoredCount++;
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
- return validPositions > 0 ? (anchoredCount / validPositions) : 0;
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
- * Disposition Effect (Loss Aversion in Time Domain).
62
- * Calculates ratio of Avg Hold Time (Losers) / Avg Hold Time (Winners).
63
- * Value > 1.0 means they hold losers longer than winners (Bad).
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 calculateDispositionEffect(historyTrades) {
66
- let winDur = 0, winCount = 0;
67
- let lossDur = 0, lossCount = 0;
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
- for (const t of historyTrades) {
70
- if (!t.OpenDateTime || !t.CloseDateTime) continue;
71
-
72
- const dur = (new Date(t.CloseDateTime) - new Date(t.OpenDateTime)) / 3600000; // Hours
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
- if (t.NetProfit > 0) {
75
- winDur += dur;
76
- winCount++;
77
- } else if (t.NetProfit < 0) {
78
- lossDur += dur;
79
- lossCount++;
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 avgWinHold = winCount > 0 ? winDur / winCount : 0;
84
- const avgLossHold = lossCount > 0 ? lossDur / lossCount : 0;
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
- if (avgWinHold === 0) return 2.0; // Infinite bias if they never hold winners
87
- return avgLossHold / avgWinHold;
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
- * Prospect Theory Utility Function (Kahneman/Tversky).
92
- * Models the psychological "utility" (pain/pleasure) of a return.
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 prospectUtility(pnl, lambda = 2.25, alpha = 0.88) {
97
- if (pnl >= 0) {
98
- return Math.pow(pnl, alpha);
99
- } else {
100
- return -lambda * Math.pow(Math.abs(pnl), alpha);
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
- // Note: Schema 5 (Insights) is an array, we assume it's converted to a map or we find it.
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
- // User PnL for today isn't explicitly stored, but Total PnL is.
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
- const { user, math, prices } = context;
204
- const history = user.history.today?.PublicHistoryPositions || [];
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
- // Penalty for biases
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: { label: label, score: Math.max(0, Math.min(100, smartScore)), isSmart: smartScore >= 65 },
251
- style: styleProfile,
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
- entryEfficiency: avgEntryEff,
254
- dispositionSkew: dispositionSkew,
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, // New
286
- SkillAttribution // New
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 > 900 * 1024) {
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
  }