Stocks Seggregation analysis.
This commit is contained in:
@@ -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,13 +17,34 @@ 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 },
|
||||||
|
|||||||
+31
-38
@@ -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',
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user