From 8ff7e5d235d2132bc321264647cc1e11f02ff9d2 Mon Sep 17 00:00:00 2001 From: Kazuma Date: Tue, 2 Jun 2026 03:31:45 -0400 Subject: [PATCH] code enhancements for hybrid analysis and clean-up dashboards. --- src/config/ScoringConfig.js | 16 ++++-- src/core/engine/ScoringEngine.js | 12 ++-- src/core/engine/ScreenerEngine.js | 43 +++++++++++++-- src/core/scorers/BondScorer.js | 4 +- src/core/scorers/EtfScorer.js | 4 +- src/core/scorers/StockScorer.js | 92 +++++++++++++++---------------- 6 files changed, 103 insertions(+), 68 deletions(-) diff --git a/src/config/ScoringConfig.js b/src/config/ScoringConfig.js index 6e0b3e4..11b45be 100644 --- a/src/config/ScoringConfig.js +++ b/src/config/ScoringConfig.js @@ -1,6 +1,12 @@ export const ScoringRules = { STOCK: { - gates: { maxDebtToEquity: 3.0, minQuickRatio: 0.25, maxPERatio: 80 }, + meta: { version: '1.2', description: 'Value & Growth Hybrid' }, + gates: { + maxDebtToEquity: 3.0, + minQuickRatio: 0.25, + maxPERatio: 80, + maxPegGate: 5.0, + }, weights: { margin: 3, peg: 2, revenue: 2, fcf: 1 }, thresholds: { marginHigh: 20, @@ -12,13 +18,15 @@ export const ScoringRules = { }, }, ETF: { - gates: { maxExpenseRatio: 0.75 }, // Reject if too expensive + 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: { - gates: { minCreditRating: 5 }, // e.g., 5 = Investment Grade + 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/engine/ScoringEngine.js b/src/core/engine/ScoringEngine.js index 0042f18..d03ff7e 100644 --- a/src/core/engine/ScoringEngine.js +++ b/src/core/engine/ScoringEngine.js @@ -1,6 +1,7 @@ import { StockScorer } from '../scorers/StockScorer.js'; import { EtfScorer } from '../scorers/EtfScorer.js'; import { BondScorer } from '../scorers/BondScorer.js'; +import { ScoringRules } from '../../config/ScoringConfig.js'; export const ScoringEngine = { // Registry of available strategies @@ -15,14 +16,15 @@ export const ScoringEngine = { * @param {Object} data - The raw metric data * @param {Object} context - Optional market context (for bonds) */ + // In ScoringEngine.js evaluate(type, data, context = {}) { const scorer = this._scorers[type]; + const rules = ScoringRules[type]; // This returns ScoringRules.STOCK - if (!scorer) { - throw new Error(`No scorer found for asset type: ${type}`); - } + if (!scorer) throw new Error(`No scorer found: ${type}`); + if (!rules) throw new Error(`No configuration rules found: ${type}`); - // Every scorer now shares the exact same signature: .score() - return scorer.score(data, context); + // IMPORTANT: Ensure you pass the specific rules set + return scorer.score(data, rules, context); }, }; diff --git a/src/core/engine/ScreenerEngine.js b/src/core/engine/ScreenerEngine.js index 0e13e2b..5e1b991 100644 --- a/src/core/engine/ScreenerEngine.js +++ b/src/core/engine/ScreenerEngine.js @@ -87,11 +87,42 @@ export class ScreenerEngine { } _display(results) { - console.log('\n--- EQUITY MATRIX ---\n'); - console.table(results.STOCK); - console.log('\n--- ETF MATRIX ---\n'); - console.table(results.ETF); - console.log('\n--- BOND MATRIX ---\n'); - console.table(results.BOND); + const formatters = { + STOCK: (data) => + data.map((item) => ({ + Ticker: item.Ticker, + Verdict: item.Verdict, + Score: item['G/O/R'], + PE: item['PE Ratio'], + PEG: item['PEG/Fee'], + Rev: item['Rev%'], + Marg: item['Marg%'], + })), + + ETF: (data) => + data.map((item) => ({ + Ticker: item.Ticker, + Verdict: item.Verdict, + Yield: item['Yield%'], + Exp: item['Exp Ratio%'], + AUM: item.AUM, + })), + + BOND: (data) => + data.map((item) => ({ + Ticker: item.Ticker, + Verdict: item.Verdict, + YTM: item['YTM%'], + Dur: item.Duration, + Rating: item.Rating, + })), + }; + + Object.keys(formatters).forEach((type) => { + if (results[type]) { + console.log(`\n--- ${type} MATRIX ---`); + console.table(formatters[type](results[type])); + } + }); } } diff --git a/src/core/scorers/BondScorer.js b/src/core/scorers/BondScorer.js index b22d642..3b96cbc 100644 --- a/src/core/scorers/BondScorer.js +++ b/src/core/scorers/BondScorer.js @@ -5,8 +5,8 @@ export const BondScorer = { * @param {Object} m - Metrics (ytm, duration, creditRating) * @param {Object} context - Market environment (riskFreeRate) */ - score(m, context) { - const { gates, weights, thresholds } = ScoringRules.BOND; + score(m, rules, context) { + const { gates, weights, thresholds } = rules; const metrics = this._sanitize(m); // Safety check for riskFreeRate (ensure it's a decimal, e.g., 0.04) diff --git a/src/core/scorers/EtfScorer.js b/src/core/scorers/EtfScorer.js index 6f4cb55..3dc27a8 100644 --- a/src/core/scorers/EtfScorer.js +++ b/src/core/scorers/EtfScorer.js @@ -4,8 +4,8 @@ import { ScoringRules } from '../../config/ScoringConfig.js'; * EtfScorer: Evaluates ETFs with mandatory fee gates and weighted scoring. */ export const EtfScorer = { - score(m) { - const { gates, weights, thresholds } = ScoringRules.ETF; + score(m, rules) { + const { gates, weights, thresholds } = rules; const metrics = this._sanitize(m); // 1. GATE KEEPING: High fees trigger an automatic reject diff --git a/src/core/scorers/StockScorer.js b/src/core/scorers/StockScorer.js index c438b16..73dce2d 100644 --- a/src/core/scorers/StockScorer.js +++ b/src/core/scorers/StockScorer.js @@ -1,37 +1,48 @@ import { ScoringRules } from '../../config/ScoringConfig.js'; -/** - * StockScorer: Evaluates stock fundamentals using a gatekeeper and weighted scoring registry. - */ export const StockScorer = { - score(m) { - const { gates, weights, thresholds } = ScoringRules.STOCK; + score(m, rules) { + const { gates, weights, thresholds } = rules; const metrics = this._sanitize(m); - // 1. GATEKEEPING + // 1. DYNAMIC GATE KEEPING const gateResult = this._checkGates(metrics, gates); - if (!gateResult.passed) { + if (!gateResult.passed) return { label: '🔴 REJECT', scoreSummary: gateResult.reason }; - } - // 2. SCORING REGISTRY - // Add new metrics here to automatically include them in the score and audit + // 2. DYNAMIC SCORING REGISTRY const scoringRegistry = [ { key: 'margin', fn: () => - this._scoreMargin(metrics.netProfitMargin, thresholds, weights), + this._scoreValue( + metrics.netProfitMargin, + thresholds.marginHigh, + thresholds.marginMed, + weights.margin, + ), }, { key: 'peg', - fn: () => this._scorePeg(metrics.pegRatio, thresholds, weights), + fn: () => + this._scorePeg( + metrics.pegRatio, + thresholds.pegHigh, + thresholds.pegMed, + weights.peg, + ), }, { key: 'rev', fn: () => - this._scoreRevenue(metrics.revenueGrowth, thresholds, weights), + this._scoreValue( + metrics.revenueGrowth, + thresholds.revHigh, + thresholds.revMed, + weights.revenue, + ), }, - { key: 'fcf', fn: () => this._scoreFcf(metrics.fcfGrowth) }, + { key: 'fcf', fn: () => (metrics.fcfGrowth === 'positive' ? 2 : -2) }, ]; const breakdown = {}; @@ -40,7 +51,6 @@ export const StockScorer = { return sum + breakdown[item.key]; }, 0); - // 3. FINAL VERDICT return { label: this._getLabel(totalScore), scoreSummary: `Score: ${totalScore}`, @@ -49,37 +59,12 @@ export const StockScorer = { }; }, - // --- Helpers --- - - _sanitize(m) { - // Helper to handle 'N/A' or nulls - const parseMetric = (val, defaultValue) => { - if (val === 'N/A' || val === null || val === undefined) - return defaultValue; - const parsed = parseFloat(val); - return isNaN(parsed) ? defaultValue : parsed; - }; - - return { - debtToEquity: parseMetric(m.debtToEquity, 0), - quickRatio: parseMetric(m.quickRatio, 0), - // If P/E is N/A, we shouldn't treat it as 999 (expensive). - // We should treat it as 'Neutral' or 'Missing' for the gate. - peRatio: parseMetric(m.peRatio, 0), - netProfitMargin: parseMetric(m.netProfitMargin, 0), - pegRatio: parseMetric(m.pegRatio, 999), - revenueGrowth: parseMetric(m.revenueGrowth, 0), - fcfGrowth: m.fcfGrowth ?? 'neutral', - }; - }, - _checkGates(m, g) { const failures = []; if (m.debtToEquity > g.maxDebtToEquity) failures.push('Debt/Equity'); if (m.quickRatio < g.minQuickRatio) failures.push('QuickRatio'); if (m.peRatio > g.maxPERatio) failures.push('PERatio'); - if (m.pegRatio > 5) failures.push('High Valuation (PEG > 5)'); - if (m.netProfitMargin < 2) failures.push('Low Profit Margin'); + if (m.pegRatio > g.maxPegGate) failures.push('High Valuation'); return { passed: failures.length === 0, @@ -87,14 +72,23 @@ export const StockScorer = { }; }, - // Scoring Methods - _scoreMargin: (val, t, w) => - val >= t.marginHigh ? w.margin : val >= t.marginMed ? 1 : -2, - _scorePeg: (val, t, w) => - val > 0 && val <= t.pegHigh ? w.peg : val <= t.pegMed ? 0 : -2, - _scoreRevenue: (val, t, w) => - val >= t.revHigh ? w.revenue : val >= t.revMed ? 0 : -1, - _scoreFcf: (val) => (val === 'positive' ? 2 : val === 'negative' ? -2 : 0), + _scoreValue: (val, high, med, weight) => + val >= high ? weight : val >= med ? 1 : -2, + + _scorePeg: (val, high, med, weight) => + val > 0 && val <= high ? weight : val <= med ? 0 : -2, + + _sanitize(m) { + return { + debtToEquity: parseFloat(m.debtToEquity) || 0, + quickRatio: parseFloat(m.quickRatio) || 0, + peRatio: parseFloat(m.peRatio) || 0, + netProfitMargin: parseFloat(m.netProfitMargin) || 0, + pegRatio: parseFloat(m.pegRatio) || 999, + revenueGrowth: parseFloat(m.revenueGrowth) || 0, + fcfGrowth: m.fcfGrowth ?? 'neutral', + }; + }, _getLabel(score) { if (score >= 5) return '🟢 BUY (High Conviction)';