benchmarks

This commit is contained in:
Kazuma
2026-06-02 02:42:41 -04:00
parent 74e6797dcc
commit d9879d5c6c
17 changed files with 512 additions and 166 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { ScreenerEngine } from './src/core/ScreenerEngine.js';
import { ScreenerEngine } from './src/core/engine/ScreenerEngine.js';
const tickers = [
'PLTR',
+47
View File
@@ -0,0 +1,47 @@
### Request: Optimize Investment Strategy Configuration
I am updating my investment strategy configuration. You are acting as a Senior Quantitative Financial Strategist. Please analyze my current market thesis and update the configuration parameters to align with this view.
**Market Thesis:** [INSERT YOUR THESIS HERE]
### Reasoning Phase (Before the JSON)
1. Briefly summarize your logic for the changes (e.g., "Raising the `maxDebtToEquity` gate because high-interest environments make capital-intensive businesses riskier").
2. Ensure all values are mathematically sound and consistent with the requested thesis.
### JSON Output Requirements
- Return a valid JSON object matching the schema below.
- Ensure all numbers are appropriate for the asset class (e.g., Debt/Equity usually 0-5, P/E usually 0-100).
- **Crucial:** Provide _only_ the JSON inside a single code block. No conversational text after the code block.
```json
{
"STOCK": {
"gates": {
"maxDebtToEquity": 0.0,
"minQuickRatio": 0.0,
"maxPERatio": 0.0
},
"weights": { "margin": 0, "peg": 0, "revenue": 0, "fcf": 0 },
"thresholds": {
"marginHigh": 0,
"marginMed": 0,
"pegHigh": 0,
"pegMed": 0,
"revHigh": 0,
"revMed": 0
}
},
"ETF": {
"gates": { "maxExpenseRatio": 0.0 },
"weights": { "yield": 0, "lowCost": 0 },
"thresholds": { "minYield": 0.0, "maxExpense": 0.0 }
},
"BOND": {
"gates": { "minCreditRating": 0 },
"weights": { "yieldSpread": 0, "duration": 0 },
"thresholds": { "minSpread": 0.0, "maxDuration": 0 }
}
}
```
+45
View File
@@ -0,0 +1,45 @@
import { YahooClient } from './YahooClient.js';
export class BenchmarkProvider {
constructor() {
this.client = new YahooClient();
this.cache = { data: null, expiresAt: 0 };
this.TTL_MS = 60 * 60 * 1000; // Cache for 1 hour
}
async getMarketContext() {
// 1. Return cached data if still valid
if (this.cache.data && Date.now() < this.cache.expiresAt) {
return this.cache.data;
}
try {
const [sp500, tn10y] = await Promise.all([
this.client.fetchSummary('^GSPC'),
this.client.fetchSummary('^TNX'),
]);
const context = {
sp500Price: sp500.price?.regularMarketPrice ?? 0,
riskFreeRate: tn10y.price?.regularMarketPrice ?? 0,
timestamp: new Date().toISOString(),
};
// 2. Validate data sanity (prevent 0-value errors)
if (context.sp500Price === 0 || context.riskFreeRate === 0) {
throw new Error('Invalid market data received (zero values)');
}
// 3. Update cache
this.cache = { data: context, expiresAt: Date.now() + this.TTL_MS };
return context;
} catch (error) {
console.error(
'Market data fetch failed, using last known or empty state:',
error,
);
// If we have stale cache, use it even if expired, otherwise return safe defaults
return this.cache.data || { sp500Price: 4500, riskFreeRate: 4.0 };
}
}
}
+3 -2
View File
@@ -1,10 +1,11 @@
// src/api/YahooClient.js
import YahooFinance from 'yahoo-finance2';
export class YahooClient {
constructor() {
// Instantiate the client as required by v3
this.yf = new YahooFinance();
this.yf = new YahooFinance({
suppressNotices: ['yahooSurvey'],
});
}
async fetchSummary(ticker, retries = 3, backoff = 1000) {
+24
View File
@@ -0,0 +1,24 @@
export const ScoringRules = {
STOCK: {
gates: { maxDebtToEquity: 3.0, minQuickRatio: 0.25, maxPERatio: 80 },
weights: { margin: 3, peg: 2, revenue: 2, fcf: 1 },
thresholds: {
marginHigh: 20,
marginMed: 10,
pegHigh: 1.3,
pegMed: 2.0,
revHigh: 15,
revMed: 5,
},
},
ETF: {
gates: { maxExpenseRatio: 0.75 }, // Reject if too expensive
weights: { yield: 2, lowCost: 3 },
thresholds: { minYield: 0.02, maxExpense: 0.2 },
},
BOND: {
gates: { minCreditRating: 5 }, // e.g., 5 = Investment Grade
weights: { yieldSpread: 3, duration: 2 },
thresholds: { minSpread: 1.5, maxDuration: 10 },
},
};
-35
View File
@@ -1,35 +0,0 @@
import { Asset } from './Asset.js';
export class Bond extends Asset {
constructor({ yieldToMaturity, duration, creditRating, ...rest }) {
super(rest);
this.yieldToMaturity = yieldToMaturity ?? 0;
this.duration = duration ?? 0;
this.creditRating = creditRating ?? 'N/A';
}
evaluate() {
let green = 0,
orange = 0,
red = 0;
// Custom Bond Rules
if (this.yieldToMaturity > 4.5) green++;
else orange++;
if (this.duration < 5) green++;
else red++;
const isStable = this.duration < 7; // < 7 years is generally lower interest-rate risk
return {
Ticker: this.ticker,
Type: 'BOND',
Price: this.formatCurrency(this.currentPrice),
'YTM%': `${this.yieldToMaturity}%`,
Duration: this.duration,
Rating: this.creditRating,
'G/O/R': `${green}/${orange}/${red}`,
Verdict: isStable ? '🟢 Stable' : '⚠️ Rate Sensitive',
};
}
}
-57
View File
@@ -1,57 +0,0 @@
import { Asset } from './Asset.js';
export class Etf extends Asset {
constructor(data) {
super(data);
this.expenseRatio = data.expenseRatio ?? 0;
this.totalAssets = data.totalAssets ?? 0;
this.yield = data.yield ?? 0;
this.fiveYearReturn = data.fiveYearReturn ?? 0;
}
evaluate() {
let green = 0,
orange = 0,
red = 0;
// Rule 1: Expense Ratio
if (this.expenseRatio !== null) {
this.expenseRatio <= 0.15 ? green++ : red++;
}
// Rule 2: Total Assets (size)
if (this.totalAssets > 0) {
this.totalAssets >= 1_000_000_000 ? green++ : red++;
}
// Rule 3: Yield
if (this.yield !== null) green++;
const isEfficient = this.expenseRatio < 0.2;
return {
Type: 'ETF',
Ticker: this.ticker,
Price: this.formatCurrency(this.currentPrice),
// Use optional chaining (?.) and nullish coalescing (?? 0)
'Exp Ratio%': `${(this.expenseRatio ?? 0).toFixed(2)}%`,
'Yield%': `${(this.yield ?? 0).toFixed(2)}%`,
AUM: this.formatLargeNumber(this.totalAssets ?? 0),
'5Y Return%': `${(this.fiveYearReturn ?? 0).toFixed(1)}%`,
Verdict: isEfficient ? '🟢 Efficient' : '🔴 High Cost', // Simplified for testing
};
}
getJustification() {
const reasons = [];
if (this.expenseRatio > 0.15)
reasons.push(`High Fee (${this.expenseRatio}%)`);
if (this.totalAssets < 1_000_000_000) reasons.push(`Low AUM`);
return {
Ticker: this.ticker,
Verdict: red > 0 ? 'Avoid' : 'Core Hold',
Reasoning: reasons.length > 0 ? reasons.join(', ') : 'Solid Foundation',
};
}
}
@@ -1,9 +1,9 @@
// src/core/Asset.js
import { ScoringEngine } from '../engine/ScoringEngine.js';
export class Asset {
constructor(data) {
this.ticker = (data.ticker || 'UNKNOWN').toUpperCase();
this.currentPrice = data.currentPrice || 0;
this.type = data.type || 'stock';
this.type = data.type || 'STOCK';
}
// Helper: Format currency safely
@@ -25,7 +25,7 @@ export class Asset {
return num.toString();
}
evaluate() {
throw new Error("Method 'evaluate()' must be implemented by subclass.");
evaluate(context) {
throw new Error('Evaluate method must be implemented by subclass');
}
}
+35
View File
@@ -0,0 +1,35 @@
import { Asset } from './Asset.js';
import { ScoringEngine } from '../engine/ScoringEngine.js';
export class Bond extends Asset {
constructor(data) {
super(data);
this.yieldToMaturity = data.yieldToMaturity ?? 0;
this.duration = data.duration ?? 0;
this.creditRating = data.creditRating ?? 'N/A';
}
evaluate(marketContext) {
// 1. Map data to the metrics the Engine expects
const metrics = {
ytm: this.yieldToMaturity,
duration: this.duration,
creditRating: this.creditRating,
};
// 2. Delegate to Engine
const scoreResult = ScoringEngine.evaluate('BOND', metrics, marketContext);
// 3. Return formatted display data
return {
Type: 'BOND',
Ticker: this.ticker,
Price: this.formatCurrency(this.currentPrice),
Verdict: scoreResult.label,
'G/O/R': scoreResult.scoreSummary,
'YTM%': `${this.yieldToMaturity.toFixed(2)}%`,
Duration: this.duration.toFixed(1),
Rating: this.creditRating,
};
}
}
+37
View File
@@ -0,0 +1,37 @@
import { Asset } from './Asset.js';
import { ScoringEngine } from '../engine/ScoringEngine.js'; // Import your engine
export class Etf extends Asset {
constructor(data) {
super(data);
this.expenseRatio = data.expenseRatio ?? 0;
this.totalAssets = data.totalAssets ?? 0;
this.yield = data.yield ?? 0;
this.fiveYearReturn = data.fiveYearReturn ?? 0;
}
evaluate(marketContext) {
// 1. Prepare metrics object for the Engine
const metrics = {
expRatio: this.expenseRatio,
totalAssets: this.totalAssets,
yield: this.yield,
};
// 2. Delegate to Engine
const scoreResult = ScoringEngine.evaluate('ETF', metrics, marketContext);
// 3. Return formatted display data
return {
Type: 'ETF',
Ticker: this.ticker,
Price: this.formatCurrency(this.currentPrice),
Verdict: scoreResult.label,
'G/O/R': scoreResult.scoreSummary,
'Exp Ratio%': `${this.expenseRatio.toFixed(2)}%`,
'Yield%': `${this.yield.toFixed(2)}%`,
AUM: this.formatLargeNumber(this.totalAssets),
'5Y Return%': `${this.fiveYearReturn.toFixed(1)}%`,
};
}
}
+29 -45
View File
@@ -1,4 +1,5 @@
import { Asset } from './Asset.js';
import { ScoringEngine } from '../engine/ScoringEngine.js';
export class Stock extends Asset {
constructor(data) {
@@ -13,6 +14,7 @@ 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
}
_detectIndustryType(summary = {}) {
@@ -43,61 +45,43 @@ export class Stock extends Asset {
}
evaluate() {
let green = 0,
orange = 0,
red = 0;
const metrics = [
{
val: this.quickRatio,
green: (v) =>
v > 1.0 ||
((this.industry === 'SaaS' || this.industry === 'Mega-Cap') &&
v >= 0.7),
orange: (v) => v >= 0.7,
},
{
val: this.debtToEquity,
green: (v) => v < 1.0,
orange: (v) => v <= 2.5 || this.industry === 'Capital-Heavy',
},
{ val: this.revenueGrowth, green: (v) => v > 10, orange: (v) => v >= 2 },
{
val: this.netProfitMargin,
green: (v) => v > 15 || this.industry === 'Retail',
orange: (v) => v >= 5,
},
{
val: this.pegRatio,
green: (v) => v > 0 && v <= 1.3,
orange: (v) => v <= 3.5 || ['SaaS', 'Mega-Cap'].includes(this.industry),
},
];
metrics.forEach((m) => {
const score = this._scoreMetric(m.val, null, m.green, m.orange);
if (score === 1) green++;
else if (score === -1) orange++;
else if (m.val !== null) red++;
});
if (this.fcfGrowth === 'positive') green++;
else if (['SaaS', 'Mega-Cap'].includes(this.industry)) orange++;
else red++;
const verdict = this.calculateVerdict(red, orange, green);
// 1. Prepare the metrics object
const metrics = {
industry: this.industry,
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
};
// 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
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
'PE Ratio': this.peRatio?.toFixed(2) ?? 'N/A',
'FCF%': formatFcf(this.fcfGrowth),
'PEG/Fee': this.pegRatio?.toFixed(2) ?? 'N/A',
'Rev%': `${this.revenueGrowth.toFixed(1)}%`,
'Marg%': `${this.netProfitMargin.toFixed(1)}%`,
Quick: this.quickRatio?.toFixed(2) ?? 'N/A',
'D/E': this.debtToEquity.toFixed(2),
'G/O/R': `${green}/${orange}/${red}`,
Verdict: verdict,
};
}
}
+28
View File
@@ -0,0 +1,28 @@
import { StockScorer } from '../scorers/StockScorer.js';
import { EtfScorer } from '../scorers/EtfScorer.js';
import { BondScorer } from '../scorers/BondScorer.js';
export const ScoringEngine = {
// Registry of available strategies
_scorers: {
STOCK: StockScorer,
ETF: EtfScorer,
BOND: BondScorer,
},
/**
* @param {string} type - 'STOCK', 'ETF', or 'BOND'
* @param {Object} data - The raw metric data
* @param {Object} context - Optional market context (for bonds)
*/
evaluate(type, data, context = {}) {
const scorer = this._scorers[type];
if (!scorer) {
throw new Error(`No scorer found for asset type: ${type}`);
}
// Every scorer now shares the exact same signature: .score()
return scorer.score(data, context);
},
};
@@ -1,13 +1,15 @@
import { YahooClient } from '../api/YahooClient.js';
import { mapToStandardFormat } from '../utils/DataMapper.js';
import { Stock } from './Stock.js';
import { Etf } from './Etf.js';
import { Bond } from './Bond.js';
import { chunkArray } from '../utils/Chunker.js';
import { YahooClient } from '../../api/YahooClient.js';
import { mapToStandardFormat } from '../../utils/DataMapper.js';
import { Stock } from '../assets/Stock.js';
import { Etf } from '../assets/Etf.js';
import { Bond } from '../assets/Bond.js';
import { chunkArray } from '../../utils/Chunker.js';
import { BenchmarkProvider } from '../../api/BenchmarkProvider.js';
export class ScreenerEngine {
constructor() {
this.client = new YahooClient();
this.benchmarkProvider = new BenchmarkProvider();
}
_createAssetInstance(data) {
@@ -39,38 +41,45 @@ export class ScreenerEngine {
}
async runParallelScreener(tickerList) {
// 1. Fetch market context (the "Ground Truth" for your scoring)
const marketContext = await this.benchmarkProvider.getMarketContext();
console.log(
`📊 Market Context Loaded: S&P500 at ${marketContext.sp500Price}, 10Y Yield at ${marketContext.riskFreeRate}%`,
);
const chunks = chunkArray(tickerList, 5);
const results = { STOCK: [], ETF: [], BOND: [] };
// Use a flexible results object that populates dynamically
const results = {};
for (const chunk of chunks) {
console.log(`🚀 Processing batch: ${chunk.join(', ')}`);
const rawDataBatch = await Promise.all(
chunk.map((t) => this._fetchAndProcess(t)),
);
rawDataBatch.forEach((data) => {
// Handle failed fetches
if (data.isError) {
results.STOCK.push(data);
if (!results['ERROR']) results['ERROR'] = [];
results['ERROR'].push(data);
return;
}
// Instantiate specific asset (Stock, ETF, or Bond)
const asset = this._createAssetInstance(data);
const evaluated = asset.evaluate();
// --- THE FIX ---
// If the evaluated.Type doesn't match a bucket,
// this console.warn will tell us exactly what key is missing.
const category = (evaluated.Type || data.type || 'STOCK').toUpperCase();
// 2. PASS the marketContext to the evaluation logic
// Ensure your Asset classes accept this in their evaluate() method
const evaluated = asset.evaluate(marketContext);
if (results[category]) {
results[category].push(evaluated);
} else {
console.warn(
`WARNING: Data dropped! Ticker ${data.ticker} has Type "${category}" which doesn't match results keys.`,
);
}
// 3. Dynamically collect results by Category (STOCK, ETF, BOND)
const category = (evaluated.Type || 'UNKNOWN').toUpperCase();
if (!results[category]) results[category] = [];
results[category].push(evaluated);
});
// Throttling to respect API rate limits
await new Promise((resolve) => setTimeout(resolve, 1000));
}
+63
View File
@@ -0,0 +1,63 @@
import { ScoringRules } from '../../config/ScoringConfig.js';
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;
const metrics = this._sanitize(m);
// Safety check for riskFreeRate (ensure it's a decimal, e.g., 0.04)
const riskFreeRate = context?.riskFreeRate ?? 0.04;
const spread = metrics.ytm - riskFreeRate;
let score = 0;
const breakdown = {};
// 1. Spread Logic: If spread is >= 0, it's at least neutral
breakdown.spread =
spread >= thresholds.minSpread ? weights.yieldSpread : -2;
// 2. Duration Logic
breakdown.duration =
metrics.duration <= thresholds.maxDuration ? weights.duration : -1;
// 3. Credit Rating Logic (Handling 'N/A')
if (metrics.creditRating === 'Junk') {
score -= 5;
}
score = Object.values(breakdown).reduce((a, b) => a + b, 0);
return {
label: this._getLabel(score),
scoreSummary: `Score: ${score}`,
audit: { breakdown },
};
},
// --- Helpers ---
_sanitize(m) {
// Convert percentage string '3.95%' to decimal 0.0395
const parsePercent = (val) => {
if (typeof val === 'string') val = val.replace('%', '');
return parseFloat(val) / 100 || 0;
};
return {
ytm: parsePercent(m.ytm),
duration: parseFloat(m.duration) || 0,
creditRating:
m.creditRating === 'N/A' ? 'InvestmentGrade' : m.creditRating, // Treat N/A as safe
};
},
_getLabel(score) {
if (score >= 4) return '🟢 Attractive';
if (score >= 1) return '🟡 Neutral';
return '🔴 Avoid';
},
};
+62
View File
@@ -0,0 +1,62 @@
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;
const metrics = this._sanitize(m);
// 1. GATE KEEPING: High fees trigger an automatic reject
if (metrics.expenseRatio > gates.maxExpenseRatio) {
return {
label: '🔴 REJECT',
scoreSummary: 'GATE FAILED: High Expense Ratio',
};
}
// 2. SCORING REGISTRY
const scoringRegistry = [
{
key: 'cost',
fn: () =>
metrics.expenseRatio <= thresholds.maxExpense ? weights.lowCost : -3,
},
{
key: 'yield',
fn: () => (metrics.yield >= thresholds.minYield ? weights.yield : -1),
},
{ key: 'vol', fn: () => (metrics.volume >= 100000 ? 0 : -2) },
];
const breakdown = {};
const totalScore = scoringRegistry.reduce((sum, item) => {
breakdown[item.key] = item.fn();
return sum + breakdown[item.key];
}, 0);
// 3. RESULT
return {
label: this._getLabel(totalScore),
scoreSummary: `Score: ${totalScore}`,
audit: { passedGates: true, breakdown },
};
},
// --- Helpers ---
_sanitize(m) {
return {
expenseRatio: parseFloat(m.expenseRatio) || 0,
yield: parseFloat(m.yield) || 0,
volume: parseFloat(m.volume) || 0,
};
},
_getLabel(score) {
if (score >= 3) return '🟢 Efficient';
if (score >= 0) return '🟡 Neutral';
return '🔴 Expensive/Low Yield';
},
};
+105
View File
@@ -0,0 +1,105 @@
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;
const metrics = this._sanitize(m);
// 1. GATEKEEPING
const gateResult = this._checkGates(metrics, gates);
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
const scoringRegistry = [
{
key: 'margin',
fn: () =>
this._scoreMargin(metrics.netProfitMargin, thresholds, weights),
},
{
key: 'peg',
fn: () => this._scorePeg(metrics.pegRatio, thresholds, weights),
},
{
key: 'rev',
fn: () =>
this._scoreRevenue(metrics.revenueGrowth, thresholds, weights),
},
{ key: 'fcf', fn: () => this._scoreFcf(metrics.fcfGrowth) },
];
const breakdown = {};
const totalScore = scoringRegistry.reduce((sum, item) => {
breakdown[item.key] = item.fn();
return sum + breakdown[item.key];
}, 0);
// 3. FINAL VERDICT
return {
label: this._getLabel(totalScore),
scoreSummary: `Score: ${totalScore}`,
analystRating: m.analystConsensus || 'N/A', // Add this
audit: { passedGates: true, breakdown },
};
},
// --- 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');
return {
passed: failures.length === 0,
reason: `GATE FAILED: ${failures.join(', ')}`,
};
},
// 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),
_getLabel(score) {
if (score >= 5) return '🟢 BUY (High Conviction)';
if (score >= 2) return '🟢 BUY (Speculative)';
if (score < -2) return '🔴 REJECT';
return '🟡 HOLD';
},
};
-2
View File
@@ -1,5 +1,3 @@
// src/utils/DataMapper.js
export const mapToStandardFormat = (ticker, summary) => {
const quoteType = summary.price?.quoteType;
const category = (summary.assetProfile?.category || '').toLowerCase();