code enhancements for hybrid analysis and clean-up dashboards.

This commit is contained in:
Saki
2026-06-02 03:31:45 -04:00
committed by saikiranvella
parent 843f74350f
commit e59cbff79c
6 changed files with 103 additions and 68 deletions
+11 -3
View File
@@ -1,6 +1,12 @@
export const ScoringRules = { export const ScoringRules = {
STOCK: { 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 }, weights: { margin: 3, peg: 2, revenue: 2, fcf: 1 },
thresholds: { thresholds: {
marginHigh: 20, marginHigh: 20,
@@ -12,12 +18,14 @@ export const ScoringRules = {
}, },
}, },
ETF: { 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 }, weights: { yield: 2, lowCost: 3 },
thresholds: { minYield: 0.02, maxExpense: 0.2 }, thresholds: { minYield: 0.02, maxExpense: 0.2 },
}, },
BOND: { 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 }, weights: { yieldSpread: 3, duration: 2 },
thresholds: { minSpread: 1.5, maxDuration: 10 }, thresholds: { minSpread: 1.5, maxDuration: 10 },
}, },
+7 -5
View File
@@ -1,6 +1,7 @@
import { StockScorer } from '../scorers/StockScorer.js'; import { StockScorer } from '../scorers/StockScorer.js';
import { EtfScorer } from '../scorers/EtfScorer.js'; import { EtfScorer } from '../scorers/EtfScorer.js';
import { BondScorer } from '../scorers/BondScorer.js'; import { BondScorer } from '../scorers/BondScorer.js';
import { ScoringRules } from '../../config/ScoringConfig.js';
export const ScoringEngine = { export const ScoringEngine = {
// Registry of available strategies // Registry of available strategies
@@ -15,14 +16,15 @@ export const ScoringEngine = {
* @param {Object} data - The raw metric data * @param {Object} data - The raw metric data
* @param {Object} context - Optional market context (for bonds) * @param {Object} context - Optional market context (for bonds)
*/ */
// In ScoringEngine.js
evaluate(type, data, context = {}) { evaluate(type, data, context = {}) {
const scorer = this._scorers[type]; const scorer = this._scorers[type];
const rules = ScoringRules[type]; // This returns ScoringRules.STOCK
if (!scorer) { if (!scorer) throw new Error(`No scorer found: ${type}`);
throw new Error(`No scorer found for asset type: ${type}`); if (!rules) throw new Error(`No configuration rules found: ${type}`);
}
// Every scorer now shares the exact same signature: .score() // IMPORTANT: Ensure you pass the specific rules set
return scorer.score(data, context); return scorer.score(data, rules, context);
}, },
}; };
+37 -6
View File
@@ -87,11 +87,42 @@ export class ScreenerEngine {
} }
_display(results) { _display(results) {
console.log('\n--- EQUITY MATRIX ---\n'); const formatters = {
console.table(results.STOCK); STOCK: (data) =>
console.log('\n--- ETF MATRIX ---\n'); data.map((item) => ({
console.table(results.ETF); Ticker: item.Ticker,
console.log('\n--- BOND MATRIX ---\n'); Verdict: item.Verdict,
console.table(results.BOND); 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]));
}
});
} }
} }
+2 -2
View File
@@ -5,8 +5,8 @@ export const BondScorer = {
* @param {Object} m - Metrics (ytm, duration, creditRating) * @param {Object} m - Metrics (ytm, duration, creditRating)
* @param {Object} context - Market environment (riskFreeRate) * @param {Object} context - Market environment (riskFreeRate)
*/ */
score(m, context) { score(m, rules, context) {
const { gates, weights, thresholds } = ScoringRules.BOND; const { gates, weights, thresholds } = rules;
const metrics = this._sanitize(m); const metrics = this._sanitize(m);
// Safety check for riskFreeRate (ensure it's a decimal, e.g., 0.04) // Safety check for riskFreeRate (ensure it's a decimal, e.g., 0.04)
+2 -2
View File
@@ -4,8 +4,8 @@ import { ScoringRules } from '../../config/ScoringConfig.js';
* EtfScorer: Evaluates ETFs with mandatory fee gates and weighted scoring. * EtfScorer: Evaluates ETFs with mandatory fee gates and weighted scoring.
*/ */
export const EtfScorer = { export const EtfScorer = {
score(m) { score(m, rules) {
const { gates, weights, thresholds } = ScoringRules.ETF; const { gates, weights, thresholds } = rules;
const metrics = this._sanitize(m); const metrics = this._sanitize(m);
// 1. GATE KEEPING: High fees trigger an automatic reject // 1. GATE KEEPING: High fees trigger an automatic reject
+43 -49
View File
@@ -1,37 +1,48 @@
import { ScoringRules } from '../../config/ScoringConfig.js'; import { ScoringRules } from '../../config/ScoringConfig.js';
/**
* StockScorer: Evaluates stock fundamentals using a gatekeeper and weighted scoring registry.
*/
export const StockScorer = { export const StockScorer = {
score(m) { score(m, rules) {
const { gates, weights, thresholds } = ScoringRules.STOCK; const { gates, weights, thresholds } = rules;
const metrics = this._sanitize(m); const metrics = this._sanitize(m);
// 1. GATEKEEPING // 1. DYNAMIC GATE KEEPING
const gateResult = this._checkGates(metrics, gates); const gateResult = this._checkGates(metrics, gates);
if (!gateResult.passed) { if (!gateResult.passed)
return { label: '🔴 REJECT', scoreSummary: gateResult.reason }; return { label: '🔴 REJECT', scoreSummary: gateResult.reason };
}
// 2. SCORING REGISTRY // 2. DYNAMIC SCORING REGISTRY
// Add new metrics here to automatically include them in the score and audit
const scoringRegistry = [ const scoringRegistry = [
{ {
key: 'margin', key: 'margin',
fn: () => fn: () =>
this._scoreMargin(metrics.netProfitMargin, thresholds, weights), this._scoreValue(
metrics.netProfitMargin,
thresholds.marginHigh,
thresholds.marginMed,
weights.margin,
),
}, },
{ {
key: 'peg', key: 'peg',
fn: () => this._scorePeg(metrics.pegRatio, thresholds, weights), fn: () =>
this._scorePeg(
metrics.pegRatio,
thresholds.pegHigh,
thresholds.pegMed,
weights.peg,
),
}, },
{ {
key: 'rev', key: 'rev',
fn: () => 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 = {}; const breakdown = {};
@@ -40,7 +51,6 @@ export const StockScorer = {
return sum + breakdown[item.key]; return sum + breakdown[item.key];
}, 0); }, 0);
// 3. FINAL VERDICT
return { return {
label: this._getLabel(totalScore), label: this._getLabel(totalScore),
scoreSummary: `Score: ${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) { _checkGates(m, g) {
const failures = []; const failures = [];
if (m.debtToEquity > g.maxDebtToEquity) failures.push('Debt/Equity'); if (m.debtToEquity > g.maxDebtToEquity) failures.push('Debt/Equity');
if (m.quickRatio < g.minQuickRatio) failures.push('QuickRatio'); if (m.quickRatio < g.minQuickRatio) failures.push('QuickRatio');
if (m.peRatio > g.maxPERatio) failures.push('PERatio'); if (m.peRatio > g.maxPERatio) failures.push('PERatio');
if (m.pegRatio > 5) failures.push('High Valuation (PEG > 5)'); if (m.pegRatio > g.maxPegGate) failures.push('High Valuation');
if (m.netProfitMargin < 2) failures.push('Low Profit Margin');
return { return {
passed: failures.length === 0, passed: failures.length === 0,
@@ -87,14 +72,23 @@ export const StockScorer = {
}; };
}, },
// Scoring Methods _scoreValue: (val, high, med, weight) =>
_scoreMargin: (val, t, w) => val >= high ? weight : val >= med ? 1 : -2,
val >= t.marginHigh ? w.margin : val >= t.marginMed ? 1 : -2,
_scorePeg: (val, t, w) => _scorePeg: (val, high, med, weight) =>
val > 0 && val <= t.pegHigh ? w.peg : val <= t.pegMed ? 0 : -2, val > 0 && val <= high ? weight : val <= med ? 0 : -2,
_scoreRevenue: (val, t, w) =>
val >= t.revHigh ? w.revenue : val >= t.revMed ? 0 : -1, _sanitize(m) {
_scoreFcf: (val) => (val === 'positive' ? 2 : val === 'negative' ? -2 : 0), 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) { _getLabel(score) {
if (score >= 5) return '🟢 BUY (High Conviction)'; if (score >= 5) return '🟢 BUY (High Conviction)';