Stocks Seggregation analysis.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export const ScoringRules = {
|
||||
// --- BASE ASSET CLASSES ---
|
||||
STOCK: {
|
||||
meta: { version: '1.2', description: 'Value & Growth Hybrid' },
|
||||
gates: {
|
||||
@@ -16,13 +17,34 @@ 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 },
|
||||
|
||||
+31
-38
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user