From a82c958771288a511b7afdfbb90c48144ab751fc Mon Sep 17 00:00:00 2001 From: Saki Date: Tue, 2 Jun 2026 03:57:05 -0400 Subject: [PATCH] Stocks Seggregation analysis. --- src/config/ScoringConfig.js | 24 ++++++++++- src/core/assets/Stock.js | 69 ++++++++++++++----------------- src/core/engine/ScreenerEngine.js | 11 ++++- src/core/scorers/StockScorer.js | 8 ++++ 4 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/config/ScoringConfig.js b/src/config/ScoringConfig.js index 11b45be..c7804d2 100644 --- a/src/config/ScoringConfig.js +++ b/src/config/ScoringConfig.js @@ -1,4 +1,5 @@ export const ScoringRules = { + // --- BASE ASSET CLASSES --- STOCK: { meta: { version: '1.2', description: 'Value & Growth Hybrid' }, gates: { @@ -16,17 +17,38 @@ export const ScoringRules = { revHigh: 15, revMed: 5, }, + + // Sector-specific intelligence + SECTOR_OVERRIDE: { + TECHNOLOGY: { + gates: { maxDebtToEquity: 1.0, minQuickRatio: 1.2, maxPegGate: 2.5 }, + weights: { margin: 2, peg: 3, revenue: 4 }, + thresholds: { marginHigh: 30, pegHigh: 1.5, revHigh: 25 }, + }, + REIT: { + gates: { maxDebtToEquity: 6.0, minQuickRatio: 0.1 }, + weights: { yield: 5, fcf: 3 }, + thresholds: { minYield: 0.05 }, + }, + FINANCIAL: { + gates: { minQuickRatio: 0.5 }, + weights: { margin: 1, revenue: 1 }, + thresholds: { marginHigh: 10 }, + }, + }, }, + ETF: { meta: { version: '1.0', description: 'Cost & Yield Efficiency' }, gates: { maxExpenseRatio: 0.75 }, weights: { yield: 2, lowCost: 3 }, thresholds: { minYield: 0.02, maxExpense: 0.2 }, }, + BOND: { meta: { version: '1.0', description: 'Credit Quality & Duration' }, gates: { minCreditRating: 5 }, weights: { yieldSpread: 3, duration: 2 }, thresholds: { minSpread: 1.5, maxDuration: 10 }, }, -}; \ No newline at end of file +}; diff --git a/src/core/assets/Stock.js b/src/core/assets/Stock.js index 993c0e8..2a54d2d 100644 --- a/src/core/assets/Stock.js +++ b/src/core/assets/Stock.js @@ -5,7 +5,8 @@ export class Stock extends Asset { constructor(data) { super(data); this.summaryData = data.summaryData; - this.industry = data.industry || this._detectIndustryType(data.summaryData); + // Map industry detection to standard sector keys used in ScoringConfig + this.sector = this._mapToStandardSector(data.summaryData); // Financial Metrics this.quickRatio = data.quickRatio ?? null; @@ -14,67 +15,59 @@ export class Stock extends Asset { this.revenueGrowth = data.revenueGrowth ?? 0; this.netProfitMargin = data.netProfitMargin ?? 0; this.pegRatio = data.pegRatio ?? null; - this.peRatio = data.peRatio ?? null; // Ensure this is included + this.peRatio = data.peRatio ?? null; } - _detectIndustryType(summary = {}) { + _mapToStandardSector(summary = {}) { const profile = summary.assetProfile || {}; const industry = (profile.industry || '').toLowerCase(); - const grossMargin = (summary.financialData?.grossMargins ?? 0) * 100; - const marketCap = summary.price?.marketCap || 0; + // Mapping logic to match your ScoringConfig.SECTOR_OVERRIDE keys if ( - grossMargin > 70 || industry.includes('software') || - industry.includes('cloud') + industry.includes('tech') || + industry.includes('semiconductor') ) - return 'SaaS'; - if (marketCap > 100_000_000_000) return 'Mega-Cap'; - if (['telecom', 'utility', 'railroad'].some((i) => industry.includes(i))) - return 'Capital-Heavy'; + return 'TECHNOLOGY'; + if (industry.includes('reit') || industry.includes('real estate')) + return 'REIT'; + if ( + industry.includes('bank') || + industry.includes('financial') || + industry.includes('insurance') + ) + return 'FINANCIAL'; - return 'General'; + return 'GENERAL'; // Maps to your base STOCK rules } - // Extracted scoring rules for cleaner 'evaluate' method - _scoreMetric(value, thresholds, isGreen, isOrange) { - if (value === null || value === undefined) return 0; // Neutral - if (isGreen(value)) return 1; // Green - if (isOrange(value)) return -1; // Orange - return -2; // Red - } - - evaluate() { - // 1. Prepare the metrics object + evaluate(marketContext) { + // 1. Prepare metrics + the detected sector const metrics = { - industry: this.industry, + sector: this.sector, // Engine uses this to pull the correct override quickRatio: this.quickRatio, debtToEquity: this.debtToEquity, revenueGrowth: this.revenueGrowth, netProfitMargin: this.netProfitMargin, pegRatio: this.pegRatio, fcfGrowth: this.fcfGrowth, - peRatio: this.peRatio, // Ensure this is mapped + peRatio: this.peRatio, }; - // 2. Delegate to the Engine - // We pass 'this.context' if you have it stored, or null - const scoreResult = ScoringEngine.evaluate('STOCK', metrics, this.context); - const formatFcf = (fcfStatus) => { - const map = { - positive: '🟢', - neutral: '🟠', - negative: '🔴', - }; - return map[fcfStatus] || 'N/A'; - }; - // 3. Return the formatted object + // 2. Delegate to Engine (which now handles the merge) + const scoreResult = ScoringEngine.evaluate('STOCK', metrics, marketContext); + + const formatFcf = (fcfStatus) => + ({ positive: '🟢', neutral: '🟠', negative: '🔴' })[fcfStatus] || 'N/A'; + + // 3. Return formatted results return { Ticker: this.ticker, Type: 'STOCK', Price: this.formatCurrency(this.currentPrice), - Verdict: scoreResult.label, // This is your official Engine verdict - 'G/O/R': scoreResult.scoreSummary, // This is your official Engine summary + Verdict: scoreResult.label, + 'G/O/R': scoreResult.scoreSummary, + Sector: this.sector, // Added for visibility 'PE Ratio': this.peRatio?.toFixed(2) ?? 'N/A', 'FCF%': formatFcf(this.fcfGrowth), 'PEG/Fee': this.pegRatio?.toFixed(2) ?? 'N/A', diff --git a/src/core/engine/ScreenerEngine.js b/src/core/engine/ScreenerEngine.js index 5e1b991..85576c7 100644 --- a/src/core/engine/ScreenerEngine.js +++ b/src/core/engine/ScreenerEngine.js @@ -91,12 +91,14 @@ export class ScreenerEngine { STOCK: (data) => data.map((item) => ({ Ticker: item.Ticker, - Verdict: item.Verdict, + Sector: item.Sector, // New: See which override rule was applied + Verdict: item.Verdict, // Now includes "High Conviction" vs "Speculative" Score: item['G/O/R'], PE: item['PE Ratio'], PEG: item['PEG/Fee'], Rev: item['Rev%'], Marg: item['Marg%'], + FCF: item['FCF%'], // Added to see the context-aware FCF score })), ETF: (data) => @@ -120,8 +122,13 @@ export class ScreenerEngine { Object.keys(formatters).forEach((type) => { if (results[type]) { + // Pro-Tip: You can now sort your data before printing to show "High Conviction" first + const sortedData = [...results[type]].sort((a, b) => + b.Verdict.localeCompare(a.Verdict), + ); + console.log(`\n--- ${type} MATRIX ---`); - console.table(formatters[type](results[type])); + console.table(formatters[type](sortedData)); } }); } diff --git a/src/core/scorers/StockScorer.js b/src/core/scorers/StockScorer.js index 73dce2d..d7ada40 100644 --- a/src/core/scorers/StockScorer.js +++ b/src/core/scorers/StockScorer.js @@ -78,6 +78,12 @@ export const StockScorer = { _scorePeg: (val, high, med, weight) => val > 0 && val <= high ? weight : val <= med ? 0 : -2, + _scoreGradient: (val, high, med, weight) => { + if (val >= high) return weight; + if (val >= med) return Math.round(weight * 0.5); // Partial credit for mid-tier + return -1; // Less punitive than -2 + }, + _sanitize(m) { return { debtToEquity: parseFloat(m.debtToEquity) || 0, @@ -87,6 +93,8 @@ export const StockScorer = { pegRatio: parseFloat(m.pegRatio) || 999, revenueGrowth: parseFloat(m.revenueGrowth) || 0, fcfGrowth: m.fcfGrowth ?? 'neutral', + dividendYield: parseFloat(m.dividendYield) || 0, + roe: parseFloat(m.roe) || 0, }; },