Files
market_screener/ui/src/lib/components/screener/GlossaryPanel.svelte
T
2026-06-09 19:34:31 -04:00

504 lines
23 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { tick } from 'svelte';
let {
open = false,
activeMetrics = [] as string[],
focusKey = null as string | null,
onClose,
}: {
open?: boolean;
activeMetrics?: string[];
focusKey?: string | null;
onClose: () => void;
} = $props();
let searchQuery = $state('');
let expandedItem = $state<string | null>(null);
let bodyEl = $state<HTMLElement | null>(null);
// When focusKey changes, expand and scroll to that item
$effect(() => {
if (focusKey && open) {
expandedItem = focusKey;
searchQuery = '';
tick().then(() => {
if (!bodyEl) return;
const el = bodyEl.querySelector(`[data-gkey="${focusKey}"]`);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
});
// ── Glossary data ─────────────────────────────────────────────────────
type RangeBand = { val: string; label: string };
type GlossaryItem = {
key: string;
label: string;
category: 'Market Context' | 'Valuation' | 'Quality' | 'Risk' | 'Signals' | 'ETF' | 'Bond';
definition: string;
gate?: string;
goodRange?: RangeBand;
neutralRange?: RangeBand;
badRange?: RangeBand;
assetTypes?: ('STOCK' | 'ETF' | 'BOND')[];
};
const GLOSSARY: GlossaryItem[] = [
// ── Market Context ─────────────────────────────────────────────────
{
key: '10Y',
label: '10Y Treasury Yield',
category: 'Market Context',
definition: 'The yield on 10-year US government bonds — the global risk-free rate benchmark. Drives discount rates for all assets: higher yield = lower present value of future earnings.',
gate: 'Rate regime: < 2% LOW | 25% NORMAL | > 5% HIGH. HIGH rates compress growth stock P/E multipliers.',
goodRange: { val: '24%', label: 'Normal, accommodative' },
neutralRange: { val: '45%', label: 'Elevated, watch growth' },
badRange: { val: '> 5%', label: 'HIGH regime, P/E compression' },
},
{
key: 'VIX',
label: 'VIX — Volatility Index',
category: 'Market Context',
definition: 'The CBOE Volatility Index — measures expected 30-day S&P 500 volatility derived from options prices. Known as the "fear gauge."',
gate: 'Volatility regime: < 15 CALM | 1525 NORMAL | > 25 ELEVATED | > 35 EXTREME',
goodRange: { val: '< 15', label: 'Calm market, low fear' },
neutralRange: { val: '1525', label: 'Normal uncertainty' },
badRange: { val: '> 25', label: 'Elevated fear' },
},
{
key: 'Rate Regime',
label: 'Rate Regime',
category: 'Market Context',
definition: 'Derived from the 10Y Treasury yield. Controls how aggressively the INFLATED scoring mode adjusts P/E gates — HIGH rates tighten the multiplier from 1.5× to 1.2× of S&P P/E.',
gate: 'LOW < 2% | NORMAL 25% | HIGH > 5%',
goodRange: { val: 'LOW', label: 'Growth-friendly' },
neutralRange: { val: 'NORMAL', label: 'Balanced' },
badRange: { val: 'HIGH', label: 'Value favoured, growth penalised' },
},
// ── Valuation ──────────────────────────────────────────────────────
{
key: 'P/E',
label: 'P/E Ratio',
category: 'Valuation',
definition: 'Price-to-Earnings: how many dollars investors pay per $1 of annual profit. Lower = cheaper relative to earnings.',
gate: 'Graham gate: ≤ 15× | Inflated gate: ≤ S&P P/E × 1.5 (live)',
goodRange: { val: '< 15×', label: 'Value / below sector avg' },
neutralRange: { val: '1535×', label: 'Elevated but common' },
badRange: { val: '> 35×', label: 'Expensive without high growth' },
assetTypes: ['STOCK'],
},
{
key: 'PEG',
label: 'PEG Ratio',
category: 'Valuation',
definition: 'P/E divided by earnings growth rate. Adjusts for growth — a 30× P/E stock growing 30% has PEG 1.0, same as a 15× stock growing 15%.',
gate: 'Gate: < 1.0 (Lynch standard) · Weight: 2',
goodRange: { val: '< 1.0', label: 'Bargain' },
neutralRange: { val: '1.02.0', label: 'Fair' },
badRange: { val: '> 2.0', label: 'Costly' },
assetTypes: ['STOCK'],
},
{
key: 'DCF Safety',
label: 'DCF Margin of Safety',
category: 'Valuation',
definition: 'How much below the discounted cash flow intrinsic value the stock trades. Positive = undervalued vs. DCF model; negative = overvalued. Requires positive FCF to compute.',
gate: '≥ +20% → full score | 020% → +1 | -200% → -1 | < -20% → negative score',
goodRange: { val: '> +20%', label: 'Significant discount' },
neutralRange: { val: '020%', label: 'Modest discount' },
badRange: { val: '< -20%', label: 'Premium to fair value' },
assetTypes: ['STOCK'],
},
{
key: 'Upside',
label: 'Analyst Price Target Upside',
category: 'Valuation',
definition: 'Percentage gap between current price and Wall Street consensus target price. Positive = analysts expect the stock to rise.',
gate: 'Risk flag if ≥ +25% upside or ≤ -15% downside',
goodRange: { val: '+520%', label: 'Moderate consensus upside' },
neutralRange: { val: '05%', label: 'Fairly priced' },
badRange: { val: '< -10%', label: 'Analysts bearish' },
assetTypes: ['STOCK'],
},
// ── Quality ────────────────────────────────────────────────────────
{
key: 'ROE%',
label: 'Return on Equity',
category: 'Quality',
definition: 'Net income as a % of shareholders\' equity. Measures how efficiently management generates profit from invested capital.',
gate: 'Gate: ROE ≥ 15%',
goodRange: { val: '> 20%', label: 'Excellent capital efficiency' },
neutralRange: { val: '1020%', label: 'Adequate' },
badRange: { val: '< 10%', label: 'Poor capital use' },
assetTypes: ['STOCK'],
},
{
key: 'OpMgn%',
label: 'Operating Margin',
category: 'Quality',
definition: 'Operating profit as a % of revenue — what\'s left after COGS and operating expenses, before interest and taxes.',
gate: 'Gate: Op Margin ≥ 10%',
goodRange: { val: '> 20%', label: 'High quality business' },
neutralRange: { val: '520%', label: 'Modest margins' },
badRange: { val: '< 5%', label: 'Thin, fragile' },
assetTypes: ['STOCK'],
},
{
key: 'GrossM%',
label: 'Gross Margin',
category: 'Quality',
definition: 'Revenue minus cost of goods sold, as a %. Shows pricing power and production efficiency before overhead.',
gate: 'Informational — not a hard gate, used contextually',
goodRange: { val: '> 50%', label: 'Software / services quality' },
neutralRange: { val: '1550%', label: 'Moderate' },
badRange: { val: '< 15%', label: 'Commodity-like, price-taker' },
assetTypes: ['STOCK'],
},
{
key: 'FCF Yld%',
label: 'Free Cash Flow Yield',
category: 'Quality',
definition: 'Free cash flow per share divided by price — cash the business actually generates, expressed as a yield. Unlike earnings, FCF is hard to fake.',
gate: 'Gate: FCF > 0 (negative FCF = gate fail) | Weight: 3× in scoring',
goodRange: { val: '> 5%', label: 'Strong cash generation' },
neutralRange: { val: '05%', label: 'Weak positive' },
badRange: { val: '< 0%', label: 'Cash-burning' },
assetTypes: ['STOCK'],
},
{
key: 'Analyst',
label: 'Analyst Consensus Rating',
category: 'Quality',
definition: 'Wall Street average recommendation on a 15 scale (Yahoo). 1 = Strong Buy, 5 = Strong Sell. Requires ≥ 3 analysts for signal to fire.',
gate: '≤ 2.0 → full score | ≤ 3.0 → +1 | ≤ 4.0 → -1 | > 4.0 → negative score',
goodRange: { val: '1.02.5', label: 'Buy consensus' },
neutralRange: { val: '2.54.0', label: 'Neutral / Hold' },
badRange: { val: '> 4.0', label: 'Sell consensus' },
assetTypes: ['STOCK'],
},
{
key: 'Revenue',
label: 'Revenue Growth',
category: 'Quality',
definition: 'Year-over-year percentage change in total revenue. Measures whether the business is expanding its top line. A secondary scoring factor — positive growth adds to score, declining revenue subtracts.',
gate: 'Gate: Revenue growth > 0% for positive contribution | Weight: 2× in scoring',
goodRange: { val: '> 10%', label: 'Strong expansion' },
neutralRange: { val: '010%', label: 'Slow growth' },
badRange: { val: '< 0%', label: 'Shrinking top line' },
assetTypes: ['STOCK'],
},
// ── Risk ───────────────────────────────────────────────────────────
{
key: 'D/E',
label: 'Debt-to-Equity Ratio',
category: 'Risk',
definition: 'Total debt divided by shareholders\' equity. Measures financial leverage — how much borrowed money vs. owned capital the company uses.',
gate: 'Gate: D/E ≤ 1.5× | Tech: ≤ 2.0× | Financials: gate disabled (scored on P/B instead)',
goodRange: { val: '< 0.5×', label: 'Conservative' },
neutralRange: { val: '0.51.5×', label: 'Moderate' },
badRange: { val: '> 2.0×', label: 'High leverage risk' },
assetTypes: ['STOCK'],
},
{
key: '52W Chg',
label: '52-Week Price Change',
category: 'Risk',
definition: 'Total % price return over the past year. Captures trend strength and momentum.',
gate: 'Risk flag: ≥ +50% (at peak, reversal risk) | ≤ -30% (significant drawdown)',
goodRange: { val: '+530%', label: 'Steady uptrend' },
neutralRange: { val: '-5+5%', label: 'Flat / sideways' },
badRange: { val: '< -30%', label: 'Significant drawdown' },
assetTypes: ['STOCK'],
},
{
key: 'From High',
label: 'Distance from 52-Week High',
category: 'Risk',
definition: 'How far (%) the current price sits below the 52-week peak. Negative = below peak. A -15% reading means the stock is 15% off its high.',
gate: 'Risk flag if > -20% from high (at or near peak)',
goodRange: { val: '-525%', label: 'Healthy pullback' },
neutralRange: { val: '-2540%', label: 'Larger drawdown' },
badRange: { val: '03%', label: 'At peak, limited buffer' },
assetTypes: ['STOCK'],
},
// ── Signals ────────────────────────────────────────────────────────
{
key: 'Graham',
label: 'Graham (Fundamental) Score',
category: 'Signals',
definition: 'Strict value-investing score using fixed Graham gates: P/E ≤ 15×, PEG ≤ 1.0, D/E ≤ 1.5×, ROE ≥ 15%, FCF > 0. Does not adjust for market conditions — these thresholds never move.',
gate: 'All gates fixed regardless of S&P P/E or rate regime',
goodRange: { val: 'PASS', label: 'Passes all Graham gates' },
neutralRange: { val: 'PARTIAL', label: 'Passes some, fails others' },
badRange: { val: 'FAIL', label: 'Fails one or more hard gates' },
},
{
key: 'Mkt-Adj',
label: 'Market-Adjusted Score',
category: 'Signals',
definition: 'Relaxed scoring mode that calibrates gates to live market benchmarks. P/E gate = S&P P/E × 1.5 (or × 1.2 in HIGH rate regime). Reflects what is "acceptable" in today\'s market, not absolute value.',
gate: 'P/E gate: S&P P/E × 1.5 (NORMAL) or × 1.2 (HIGH) | Tech P/E: XLK P/E × 1.3',
goodRange: { val: 'PASS', label: 'Passes mkt-adjusted gates' },
neutralRange: { val: 'PARTIAL', label: 'Borderline vs live benchmarks' },
badRange: { val: 'FAIL', label: 'Fails even relaxed gates' },
},
{
key: 'signal',
label: 'Signal',
category: 'Signals',
definition: 'Overall recommendation derived by comparing Market-Adjusted and Graham (fundamental) scores.',
gate: 'Strong Buy = passes both | Momentum = passes Mkt-Adj only | Speculation = passes Mkt-Adj, fails Graham | Neutral = borderline | Avoid = fails both',
goodRange: { val: '✅ ⚡', label: 'Strong Buy / Momentum' },
neutralRange: { val: '🔄', label: 'Neutral — hold' },
badRange: { val: '⚠️ ❌', label: 'Speculation / Avoid' },
},
{
key: 'score',
label: 'Score (dot scale)',
category: 'Signals',
definition: 'Weighted sum of factor scores (ROE, FCF, margin, PEG, revenue growth, analyst, DCF). Displayed as ●●●●○ dots out of 5 + raw number.',
gate: 'Positive factors add to score; negative riskFlags subtract. Gate failures bypass scoring entirely (shown as ✗).',
goodRange: { val: '> 12', label: 'High conviction' },
neutralRange: { val: '612', label: 'Borderline' },
badRange: { val: '< 6', label: 'Weak factors' },
},
{
key: 'Cap Tier',
label: 'Market Cap Tier',
category: 'Signals',
definition: 'Size classification based on market capitalisation. Mega Cap (> $200B), Large ($10200B), Mid ($210B), Small ($300M$2B), Micro (< $300M).',
gate: 'Informational — not a gate. Useful for position sizing and risk calibration.',
goodRange: { val: 'Mega / Large', label: 'Most liquid' },
neutralRange: { val: 'Mid', label: 'Balanced' },
badRange: { val: 'Micro', label: 'High vol, thin liquidity' },
assetTypes: ['STOCK'],
},
{
key: 'Style',
label: 'Growth / Style Category',
category: 'Signals',
definition: 'Derived from revenue growth and earnings growth. High Growth (rev ≥ 15% or earnings ≥ 20%), Growth (515%), Value (< 5% + yield ≥ 3%), Stable, Turnaround, Declining.',
gate: 'Informational — not a gate. Helps match the stock to your strategy.',
goodRange: { val: 'High Growth / Growth', label: 'Matches momentum strategy' },
neutralRange: { val: 'Stable / Value', label: 'Income / defensive' },
badRange: { val: 'Declining', label: 'Revenue shrinking > -5%' },
assetTypes: ['STOCK'],
},
// ── ETF ────────────────────────────────────────────────────────────
{
key: 'Exp Ratio%',
label: 'Expense Ratio',
category: 'ETF',
definition: 'Annual management fee as a % of AUM. Deducted from returns automatically. Lower is always better — costs compound against you.',
gate: 'Hard gate: Expense Ratio ≤ 0.20%',
goodRange: { val: '< 0.10%', label: 'Index-like, minimal drag' },
neutralRange: { val: '0.100.50%', label: 'Acceptable' },
badRange: { val: '> 0.50%', label: 'High cost drag' },
assetTypes: ['ETF'],
},
{
key: '5Y Return%',
label: '5-Year Annualised Return',
category: 'ETF',
definition: 'Compound annual growth rate over 5 years. The S&P 500 long-run average is ~10%; use that as a baseline.',
gate: 'Gate: 5Y Return ≥ 8% (S&P long-run floor)',
goodRange: { val: '> 12%', label: 'Outperforming market' },
neutralRange: { val: '812%', label: 'Market-rate returns' },
badRange: { val: '< 6%', label: 'Underperforming bonds + inflation' },
assetTypes: ['ETF'],
},
{
key: 'Yield%',
label: 'Distribution Yield',
category: 'ETF',
definition: 'Annual income distributions (dividends, interest) as a % of NAV. Important for income-focused or REIT ETFs.',
gate: 'REIT ETF: Yield floor based on XLRE yield × regime factor',
goodRange: { val: '> 3%', label: 'Strong income' },
neutralRange: { val: '13%', label: 'Low but positive' },
badRange: { val: '< 1%', label: 'Insufficient for income' },
assetTypes: ['ETF'],
},
// ── Bond ───────────────────────────────────────────────────────────
{
key: 'YTM%',
label: 'Yield to Maturity',
category: 'Bond',
definition: 'Total return if you hold the bond to maturity — includes coupon payments plus any price gain/loss vs. par. The true all-in yield.',
gate: 'Spread gate: YTM must exceed risk-free rate by ≥ 1.5% (NORMAL) or ≥ 1.8% (HIGH rates)',
goodRange: { val: 'Sprd > 2%', label: 'Good compensation for risk' },
neutralRange: { val: '12%', label: 'Adequate spread' },
badRange: { val: '< 1%', label: 'Not compensating for credit risk' },
assetTypes: ['BOND'],
},
{
key: 'Duration',
label: 'Duration (years)',
category: 'Bond',
definition: 'Sensitivity to interest rate changes. A duration of 5 means a 1% rate rise → ~5% price drop. Shorter = less rate risk.',
gate: 'Gate: Duration ≤ 7 years',
goodRange: { val: '< 4 yrs', label: 'Low rate sensitivity' },
neutralRange: { val: '47 yrs', label: 'Moderate' },
badRange: { val: '> 10 yrs', label: 'High rate risk' },
assetTypes: ['BOND'],
},
{
key: 'Rating',
label: 'Credit Rating',
category: 'Bond',
definition: 'Agency rating of default probability: AAA (safest) → AA → A → BBB (investment grade floor) → BB → B → CCC (junk).',
gate: 'Hard gate: Rating ≥ BBB (investment-grade, numeric ≥ 7)',
goodRange: { val: 'AAAA', label: 'Very low default risk' },
neutralRange: { val: 'BBB', label: 'Investment-grade floor' },
badRange: { val: '≤ BB', label: 'High yield / junk' },
assetTypes: ['BOND'],
},
];
const CATEGORIES = ['Market Context', 'Valuation', 'Quality', 'Risk', 'Signals', 'ETF', 'Bond'] as const;
function filteredItems(): GlossaryItem[] {
const q = searchQuery.trim().toLowerCase();
if (!q) return GLOSSARY;
return GLOSSARY.filter(
(item) =>
item.label.toLowerCase().includes(q) ||
item.definition.toLowerCase().includes(q) ||
item.category.toLowerCase().includes(q),
);
}
function itemsForCategory(cat: string): GlossaryItem[] {
return filteredItems().filter((i) => i.category === cat);
}
function isActive(item: GlossaryItem): boolean {
return activeMetrics.some(
(k) => k === item.key || k === item.label,
);
}
function toggleItem(key: string) {
expandedItem = expandedItem === key ? null : key;
}
// Close on Escape
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
</script>
<svelte:window onkeydown={handleKeydown} />
{#if open}
<!-- Click-outside backdrop — thin, no visual overlay, just captures clicks -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="glossary-backdrop" onclick={onClose}></div>
<aside class="glossary-panel" aria-label="Metrics Glossary">
<!-- Header -->
<div class="glossary-header">
<span class="glossary-title"><span class="glossary-title-q">?</span> Metric Glossary</span>
<button class="glossary-close" onclick={onClose} aria-label="Close glossary">×</button>
</div>
<!-- Search -->
<div class="glossary-search-wrap">
<input
class="glossary-search"
type="text"
placeholder="Search metrics…"
bind:value={searchQuery}
aria-label="Search glossary"
/>
{#if searchQuery}
<button class="glossary-search-clear" onclick={() => (searchQuery = '')} aria-label="Clear search"></button>
{/if}
</div>
<!-- Context banner — fixed between search and body, only when row is selected -->
{#if activeMetrics.length > 0}
<div class="glossary-ctx-banner">
✦ Highlighted metrics are relevant to the selected row
</div>
{/if}
<!-- Body -->
<div class="glossary-body" bind:this={bodyEl}>
{#each CATEGORIES as cat}
{@const items = itemsForCategory(cat)}
{#if items.length > 0}
<div class="glossary-category">
<div class="glossary-cat-header">{cat}</div>
{#each items as item}
{@const active = isActive(item)}
{@const isExpanded = expandedItem === item.key}
<div
class="glossary-item"
class:glossary-item-active={active}
class:glossary-item-open={isExpanded}
data-gkey={item.key}
>
<button
class="glossary-item-trigger"
onclick={() => toggleItem(item.key)}
aria-expanded={isExpanded}
>
<span class="glossary-item-label">
{#if active}<span class="glossary-active-dot"></span>{/if}
{item.label}
</span>
<span class="glossary-cat-tag gcat-{cat.toLowerCase().replace(/\s/g,'-')}">{cat}</span>
</button>
{#if isExpanded}
<div class="glossary-item-body">
<p class="glossary-definition">{item.definition}</p>
{#if item.gate}
<div class="glossary-gate-box">
<code>{item.gate}</code>
</div>
{/if}
{#if item.goodRange || item.neutralRange || item.badRange}
<div class="glossary-range-pills">
{#if item.goodRange}
<span class="glossary-range-pill grange-good">{item.goodRange.val}</span>
{/if}
{#if item.neutralRange}
<span class="glossary-range-pill grange-neutral">{item.neutralRange.val}</span>
{/if}
{#if item.badRange}
<span class="glossary-range-pill grange-bad">{item.badRange.val}</span>
{/if}
</div>
<div class="glossary-range-labels">
{#if item.goodRange}
<span class="grlabel-good">{item.goodRange.label}</span>
{/if}
{#if item.neutralRange}
<span class="grlabel-neutral">{item.neutralRange.label}</span>
{/if}
{#if item.badRange}
<span class="grlabel-bad">{item.badRange.label}</span>
{/if}
</div>
{/if}
</div>
{/if}
</div>
{/each}
</div>
{/if}
{/each}
{#if filteredItems().length === 0}
<div class="glossary-empty">No metrics match "{searchQuery}"</div>
{/if}
</div>
</aside>
{/if}