Stocks Seggregation analysis.

This commit is contained in:
Saki
2026-06-02 03:57:05 -04:00
committed by saikiranvella
parent e59cbff79c
commit 341c816e61
4 changed files with 71 additions and 41 deletions
+23 -1
View File
@@ -1,4 +1,5 @@
export const ScoringRules = { export const ScoringRules = {
// --- BASE ASSET CLASSES ---
STOCK: { STOCK: {
meta: { version: '1.2', description: 'Value & Growth Hybrid' }, meta: { version: '1.2', description: 'Value & Growth Hybrid' },
gates: { gates: {
@@ -16,17 +17,38 @@ export const ScoringRules = {
revHigh: 15, revHigh: 15,
revMed: 5, 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: { ETF: {
meta: { version: '1.0', description: 'Cost & Yield Efficiency' }, meta: { version: '1.0', description: 'Cost & Yield Efficiency' },
gates: { maxExpenseRatio: 0.75 }, 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: {
meta: { version: '1.0', description: 'Credit Quality & Duration' }, meta: { version: '1.0', description: 'Credit Quality & Duration' },
gates: { minCreditRating: 5 }, 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 },
}, },
}; };
+31 -38
View File
@@ -5,7 +5,8 @@ export class Stock extends Asset {
constructor(data) { constructor(data) {
super(data); super(data);
this.summaryData = data.summaryData; 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 // Financial Metrics
this.quickRatio = data.quickRatio ?? null; this.quickRatio = data.quickRatio ?? null;
@@ -14,67 +15,59 @@ export class Stock extends Asset {
this.revenueGrowth = data.revenueGrowth ?? 0; this.revenueGrowth = data.revenueGrowth ?? 0;
this.netProfitMargin = data.netProfitMargin ?? 0; this.netProfitMargin = data.netProfitMargin ?? 0;
this.pegRatio = data.pegRatio ?? null; 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 profile = summary.assetProfile || {};
const industry = (profile.industry || '').toLowerCase(); 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 ( if (
grossMargin > 70 ||
industry.includes('software') || industry.includes('software') ||
industry.includes('cloud') industry.includes('tech') ||
industry.includes('semiconductor')
) )
return 'SaaS'; return 'TECHNOLOGY';
if (marketCap > 100_000_000_000) return 'Mega-Cap'; if (industry.includes('reit') || industry.includes('real estate'))
if (['telecom', 'utility', 'railroad'].some((i) => industry.includes(i))) return 'REIT';
return 'Capital-Heavy'; 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 evaluate(marketContext) {
_scoreMetric(value, thresholds, isGreen, isOrange) { // 1. Prepare metrics + the detected sector
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
const metrics = { const metrics = {
industry: this.industry, sector: this.sector, // Engine uses this to pull the correct override
quickRatio: this.quickRatio, quickRatio: this.quickRatio,
debtToEquity: this.debtToEquity, debtToEquity: this.debtToEquity,
revenueGrowth: this.revenueGrowth, revenueGrowth: this.revenueGrowth,
netProfitMargin: this.netProfitMargin, netProfitMargin: this.netProfitMargin,
pegRatio: this.pegRatio, pegRatio: this.pegRatio,
fcfGrowth: this.fcfGrowth, fcfGrowth: this.fcfGrowth,
peRatio: this.peRatio, // Ensure this is mapped peRatio: this.peRatio,
}; };
// 2. Delegate to the Engine // 2. Delegate to Engine (which now handles the merge)
// We pass 'this.context' if you have it stored, or null const scoreResult = ScoringEngine.evaluate('STOCK', metrics, marketContext);
const scoreResult = ScoringEngine.evaluate('STOCK', metrics, this.context);
const formatFcf = (fcfStatus) => { const formatFcf = (fcfStatus) =>
const map = { ({ positive: '🟢', neutral: '🟠', negative: '🔴' })[fcfStatus] || 'N/A';
positive: '🟢',
neutral: '🟠', // 3. Return formatted results
negative: '🔴',
};
return map[fcfStatus] || 'N/A';
};
// 3. Return the formatted object
return { return {
Ticker: this.ticker, Ticker: this.ticker,
Type: 'STOCK', Type: 'STOCK',
Price: this.formatCurrency(this.currentPrice), Price: this.formatCurrency(this.currentPrice),
Verdict: scoreResult.label, // This is your official Engine verdict Verdict: scoreResult.label,
'G/O/R': scoreResult.scoreSummary, // This is your official Engine summary 'G/O/R': scoreResult.scoreSummary,
Sector: this.sector, // Added for visibility
'PE Ratio': this.peRatio?.toFixed(2) ?? 'N/A', 'PE Ratio': this.peRatio?.toFixed(2) ?? 'N/A',
'FCF%': formatFcf(this.fcfGrowth), 'FCF%': formatFcf(this.fcfGrowth),
'PEG/Fee': this.pegRatio?.toFixed(2) ?? 'N/A', 'PEG/Fee': this.pegRatio?.toFixed(2) ?? 'N/A',
+9 -2
View File
@@ -91,12 +91,14 @@ export class ScreenerEngine {
STOCK: (data) => STOCK: (data) =>
data.map((item) => ({ data.map((item) => ({
Ticker: item.Ticker, 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'], Score: item['G/O/R'],
PE: item['PE Ratio'], PE: item['PE Ratio'],
PEG: item['PEG/Fee'], PEG: item['PEG/Fee'],
Rev: item['Rev%'], Rev: item['Rev%'],
Marg: item['Marg%'], Marg: item['Marg%'],
FCF: item['FCF%'], // Added to see the context-aware FCF score
})), })),
ETF: (data) => ETF: (data) =>
@@ -120,8 +122,13 @@ export class ScreenerEngine {
Object.keys(formatters).forEach((type) => { Object.keys(formatters).forEach((type) => {
if (results[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.log(`\n--- ${type} MATRIX ---`);
console.table(formatters[type](results[type])); console.table(formatters[type](sortedData));
} }
}); });
} }
+8
View File
@@ -78,6 +78,12 @@ export const StockScorer = {
_scorePeg: (val, high, med, weight) => _scorePeg: (val, high, med, weight) =>
val > 0 && val <= high ? weight : val <= med ? 0 : -2, 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) { _sanitize(m) {
return { return {
debtToEquity: parseFloat(m.debtToEquity) || 0, debtToEquity: parseFloat(m.debtToEquity) || 0,
@@ -87,6 +93,8 @@ export const StockScorer = {
pegRatio: parseFloat(m.pegRatio) || 999, pegRatio: parseFloat(m.pegRatio) || 999,
revenueGrowth: parseFloat(m.revenueGrowth) || 0, revenueGrowth: parseFloat(m.revenueGrowth) || 0,
fcfGrowth: m.fcfGrowth ?? 'neutral', fcfGrowth: m.fcfGrowth ?? 'neutral',
dividendYield: parseFloat(m.dividendYield) || 0,
roe: parseFloat(m.roe) || 0,
}; };
}, },