106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
/**
|
|
* Shared pure utility functions used across screener, portfolio, and safe-buys pages.
|
|
* All functions are stateless and framework-agnostic.
|
|
*/
|
|
|
|
// ── Signal ordering ───────────────────────────────────────────────────────────
|
|
|
|
export type Signal =
|
|
| '✅ Strong Buy'
|
|
| '⚡ Momentum'
|
|
| '🔄 Neutral'
|
|
| '⚠️ Speculation'
|
|
| '❌ Avoid';
|
|
|
|
const SIGNAL_ORDER: Record<string, number> = {
|
|
'✅ Strong Buy': 0,
|
|
'⚡ Momentum': 1,
|
|
'🔄 Neutral': 2,
|
|
'⚠️ Speculation': 3,
|
|
'❌ Avoid': 4,
|
|
};
|
|
|
|
/** Returns sort order for a signal string (lower = stronger). Unknown signals → 5. */
|
|
export function sigOrd(signal: string | null | undefined): number {
|
|
return SIGNAL_ORDER[signal ?? ''] ?? 5;
|
|
}
|
|
|
|
/** Sorts an array of screener result rows by signal strength (strongest first). */
|
|
export function sorted<T extends { signal?: string | null }>(arr: T[]): T[] {
|
|
return [...(arr ?? [])].sort((a, b) => sigOrd(a.signal) - sigOrd(b.signal));
|
|
}
|
|
|
|
// ── Verdict label helpers ─────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Converts a long verdict label into a short display string.
|
|
* e.g. "🟢 BUY (High Conviction)" → "Strong"
|
|
*/
|
|
export function verdictShort(label: string | null | undefined): string {
|
|
if (!label) return '—';
|
|
if (label.includes('High Conviction')) return 'Strong';
|
|
if (label.includes('Speculative')) return 'Speculative';
|
|
if (label.includes('BUY')) return 'Buy';
|
|
if (label.includes('Efficient')) return 'Efficient';
|
|
if (label.includes('Attractive')) return 'Attractive';
|
|
if (label.includes('Neutral')) return 'Hold';
|
|
if (label.includes('REJECT')) return 'Reject';
|
|
if (label.includes('Avoid')) return 'Avoid';
|
|
return label.replace(/[🟢🟡🔴]/u, '').trim();
|
|
}
|
|
|
|
/**
|
|
* Returns a CSS colour class ('green' | 'yellow' | 'red') based on
|
|
* the emoji prefix of a verdict label.
|
|
*/
|
|
export function vClass(label: string | null | undefined): 'green' | 'yellow' | 'red' {
|
|
if (label?.startsWith('🟢')) return 'green';
|
|
if (label?.startsWith('🟡')) return 'yellow';
|
|
return 'red';
|
|
}
|
|
|
|
// ── Number formatters ─────────────────────────────────────────────────────────
|
|
|
|
/** Formats a P/E ratio — e.g. 22.5 → "22.5x", null → "—" */
|
|
export function fmtPE(v: number | null | undefined): string {
|
|
return v != null ? v + 'x' : '—';
|
|
}
|
|
|
|
/** Full currency format — e.g. 1234.5 → "$1,234.50" */
|
|
export function fmt(n: number | null | undefined): string {
|
|
return n != null
|
|
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(n)
|
|
: '—';
|
|
}
|
|
|
|
/** Compact currency format (no cents) — e.g. 1234.5 → "$1,235" */
|
|
export function fmtShort(n: number | null | undefined): string {
|
|
return n != null
|
|
? new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD',
|
|
maximumFractionDigits: 0,
|
|
}).format(n)
|
|
: '—';
|
|
}
|
|
|
|
/**
|
|
* Returns 'green' for non-negative G/L percentage, 'red' otherwise.
|
|
* Accepts string (e.g. "12.5") or number.
|
|
*/
|
|
export function glClass(pct: string | number | null | undefined): 'green' | 'red' {
|
|
return parseFloat(String(pct ?? 0)) >= 0 ? 'green' : 'red';
|
|
}
|
|
|
|
/**
|
|
* Returns a CSS colour class for a portfolio advice string based on its emoji prefix.
|
|
* 🟢 → 'green', 🟡 → 'yellow', 🟠 → 'orange', 🔴 → 'red', else 'gray'.
|
|
*/
|
|
export function advClass(advice: string | null | undefined): 'green' | 'yellow' | 'orange' | 'red' | 'gray' {
|
|
if (advice?.includes('🟢')) return 'green';
|
|
if (advice?.includes('🟡')) return 'yellow';
|
|
if (advice?.includes('🟠')) return 'orange';
|
|
if (advice?.includes('🔴')) return 'red';
|
|
return 'gray';
|
|
}
|