phase-9: domain-driven architecture complete
- Restructured server layer with 5 domains: shared, screener, portfolio, calls, finance - Migrated 58 TypeScript files to domain-driven structure - Updated CLAUDE.md with new architecture documentation - Added .gitignore rules for .md files (except CLAUDE.md) - Removed unused CatalystAnalyst import from app.ts - Fixed lint errors: removed unused imports, fixed regex escape, added console suppressions - Verified no sensitive data in git history - Server code compiles cleanly with TypeScript strict mode
This commit is contained in:
committed by
saikiranvella
parent
c7e39c3e4e
commit
c388b6d83c
@@ -0,0 +1,222 @@
|
||||
import { Asset } from './Asset';
|
||||
import { CAP_CATEGORY, GROWTH_CATEGORY } from '../config/constants';
|
||||
import type { Sector, CapCategory, GrowthCategory } from '../config/constants';
|
||||
import type { StockData, StockMetrics } from '../types/models.model';
|
||||
|
||||
export class Stock extends Asset {
|
||||
sector: Sector;
|
||||
metrics: StockMetrics;
|
||||
|
||||
constructor(data: StockData) {
|
||||
super(data);
|
||||
this.sector = this.mapToStandardSector(data);
|
||||
|
||||
this.metrics = {
|
||||
sector: this.sector,
|
||||
capCategory: this.classifyMarketCap(data.marketCap ?? null),
|
||||
growthCategory: this.classifyGrowth(
|
||||
data.revenueGrowth ?? null,
|
||||
data.earningsGrowth ?? null,
|
||||
data.dividendYield ?? null,
|
||||
),
|
||||
peRatio: data.peRatio ?? null,
|
||||
pegRatio: data.pegRatio ?? null,
|
||||
priceToBook: data.priceToBook ?? null,
|
||||
grossMargin: data.grossMargin ?? null,
|
||||
netProfitMargin: data.netProfitMargin ?? null,
|
||||
operatingMargin: data.operatingMargin ?? null,
|
||||
returnOnEquity: data.returnOnEquity ?? null,
|
||||
revenueGrowth: data.revenueGrowth ?? null,
|
||||
earningsGrowth: data.earningsGrowth ?? null,
|
||||
debtToEquity: data.debtToEquity ?? null,
|
||||
quickRatio: data.quickRatio ?? null,
|
||||
fcfYield: data.fcfYield ?? null,
|
||||
pFFO: data.pFFO ?? null,
|
||||
dividendYield: data.dividendYield ?? null,
|
||||
beta: data.beta ?? null,
|
||||
week52High: data.week52High ?? null,
|
||||
week52Low: data.week52Low ?? null,
|
||||
week52Change: data.week52Change ?? null,
|
||||
week52FromHigh: data.week52FromHigh ?? null,
|
||||
week52FromLow: data.week52FromLow ?? null,
|
||||
marketCap: data.marketCap ?? null,
|
||||
analystRating: data.analystRating ?? null,
|
||||
analystTargetPrice: data.analystTargetPrice ?? null,
|
||||
analystUpside: data.analystUpside ?? null,
|
||||
numberOfAnalysts: data.numberOfAnalysts ?? null,
|
||||
dcfIntrinsicValue: data.dcfIntrinsicValue ?? null,
|
||||
dcfMarginOfSafety: data.dcfMarginOfSafety ?? null,
|
||||
currentPrice: (data.currentPrice as number) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Market cap tier classification ──────────────────────────────────────
|
||||
// Thresholds follow MSCI/Russell institutional convention.
|
||||
classifyMarketCap(marketCap: number | null): CapCategory {
|
||||
if (marketCap == null) return CAP_CATEGORY.LARGE; // safe default
|
||||
if (marketCap >= 200e9) return CAP_CATEGORY.MEGA;
|
||||
if (marketCap >= 10e9) return CAP_CATEGORY.LARGE;
|
||||
if (marketCap >= 2e9) return CAP_CATEGORY.MID;
|
||||
if (marketCap >= 300e6) return CAP_CATEGORY.SMALL;
|
||||
return CAP_CATEGORY.MICRO;
|
||||
}
|
||||
|
||||
// ── Growth / style classification ───────────────────────────────────────
|
||||
// revenueGrowth and earningsGrowth are in percentage form (e.g. 15 = 15%).
|
||||
// dividendYield is also in percentage form (e.g. 3.5 = 3.5%).
|
||||
classifyGrowth(
|
||||
revenueGrowth: number | null,
|
||||
earningsGrowth: number | null,
|
||||
dividendYield: number | null,
|
||||
): GrowthCategory {
|
||||
const rev = revenueGrowth ?? 0;
|
||||
const earn = earningsGrowth ?? 0;
|
||||
const div = dividendYield ?? 0;
|
||||
|
||||
if (rev < -5) return GROWTH_CATEGORY.DECLINING;
|
||||
if (earn < 0 && rev >= 0) return GROWTH_CATEGORY.TURNAROUND;
|
||||
if (rev >= 15 || earn >= 20) return GROWTH_CATEGORY.HIGH_GROWTH;
|
||||
if (rev >= 5) return GROWTH_CATEGORY.MODERATE_GROWTH;
|
||||
if (div >= 3 && rev < 5) return GROWTH_CATEGORY.VALUE;
|
||||
return GROWTH_CATEGORY.STABLE;
|
||||
}
|
||||
|
||||
mapToStandardSector(data: StockData): Sector {
|
||||
const profile = data.assetProfile ?? {};
|
||||
const industry = (profile.industry || '').toLowerCase();
|
||||
const sector = (profile.sector || '').toLowerCase();
|
||||
const combined = `${industry} ${sector}`;
|
||||
|
||||
if (
|
||||
combined.includes('technology') ||
|
||||
combined.includes('electronic') ||
|
||||
combined.includes('semiconductor') ||
|
||||
combined.includes('software')
|
||||
)
|
||||
return 'TECHNOLOGY';
|
||||
if (combined.includes('real estate') || combined.includes('reit')) return 'REIT';
|
||||
if (
|
||||
combined.includes('financial') ||
|
||||
combined.includes('bank') ||
|
||||
combined.includes('insurance') ||
|
||||
combined.includes('asset management')
|
||||
)
|
||||
return 'FINANCIAL';
|
||||
if (
|
||||
combined.includes('energy') ||
|
||||
combined.includes('oil') ||
|
||||
combined.includes('gas') ||
|
||||
combined.includes('petroleum')
|
||||
)
|
||||
return 'ENERGY';
|
||||
if (
|
||||
combined.includes('health') ||
|
||||
combined.includes('biotech') ||
|
||||
combined.includes('pharmaceutical') ||
|
||||
combined.includes('medical')
|
||||
)
|
||||
return 'HEALTHCARE';
|
||||
if (
|
||||
combined.includes('communication') ||
|
||||
combined.includes('media') ||
|
||||
combined.includes('entertainment') ||
|
||||
combined.includes('telecom')
|
||||
)
|
||||
return 'COMMUNICATION';
|
||||
if (
|
||||
combined.includes('consumer defensive') ||
|
||||
combined.includes('consumer staples') ||
|
||||
combined.includes('household') ||
|
||||
combined.includes('beverage') ||
|
||||
combined.includes('food')
|
||||
)
|
||||
return 'CONSUMER_STAPLES';
|
||||
if (
|
||||
combined.includes('consumer cyclical') ||
|
||||
combined.includes('consumer discretionary') ||
|
||||
combined.includes('retail') ||
|
||||
combined.includes('apparel') ||
|
||||
combined.includes('auto')
|
||||
)
|
||||
return 'CONSUMER_DISCRETIONARY';
|
||||
|
||||
return 'GENERAL';
|
||||
}
|
||||
|
||||
getDisplayMetrics(): Record<string, string | null> {
|
||||
const fmt = (v: number | null, dec = 1, suffix = '') =>
|
||||
v != null ? `${v.toFixed(dec)}${suffix}` : null;
|
||||
const fmtSign = (v: number | null, suffix = '%') =>
|
||||
v != null ? `${v >= 0 ? '+' : ''}${v.toFixed(1)}${suffix}` : null;
|
||||
const m = this.metrics;
|
||||
|
||||
const w52pos =
|
||||
m.week52High != null && m.week52High > 0 && m.week52Low != null && m.currentPrice > 0
|
||||
? (((m.currentPrice - m.week52Low) / (m.week52High - m.week52Low)) * 100).toFixed(0) + '%'
|
||||
: null;
|
||||
|
||||
// Analyst label: convert Yahoo's 1–5 scale to a readable string
|
||||
const analystLabel = (rating: number | null): string | null => {
|
||||
if (rating == null) return null;
|
||||
if (rating <= 1.5) return 'Strong Buy';
|
||||
if (rating <= 2.5) return 'Buy';
|
||||
if (rating <= 3.5) return 'Hold';
|
||||
if (rating <= 4.5) return 'Sell';
|
||||
return 'Strong Sell';
|
||||
};
|
||||
|
||||
const display: Record<string, string | null> = {
|
||||
Ticker: this.ticker,
|
||||
Price: this.formatCurrency(this.currentPrice),
|
||||
Sector: this.sector,
|
||||
'Cap Tier': m.capCategory,
|
||||
Style: m.growthCategory,
|
||||
};
|
||||
|
||||
// Valuation
|
||||
if (m.peRatio != null) display['P/E'] = fmt(m.peRatio, 1);
|
||||
if (m.pegRatio != null) display['PEG'] = fmt(m.pegRatio, 2);
|
||||
if (m.priceToBook != null) display['P/B'] = fmt(m.priceToBook, 2);
|
||||
|
||||
// Quality
|
||||
if (m.grossMargin != null) display['GrossM%'] = fmt(m.grossMargin, 1, '%');
|
||||
if (m.returnOnEquity != null) display['ROE%'] = fmt(m.returnOnEquity, 1, '%');
|
||||
if (m.operatingMargin != null) display['OpMgn%'] = fmt(m.operatingMargin, 1, '%');
|
||||
if (m.netProfitMargin != null) display['NetMgn%'] = fmt(m.netProfitMargin, 1, '%');
|
||||
if (m.revenueGrowth != null) display['Rev%'] = fmt(m.revenueGrowth, 1, '%');
|
||||
if (m.fcfYield != null) display['FCF Yld%'] = fmt(m.fcfYield, 1, '%');
|
||||
if (m.dividendYield != null) display['Div%'] = fmt(m.dividendYield, 2, '%');
|
||||
|
||||
// Risk
|
||||
if (m.debtToEquity != null) display['D/E'] = fmt(m.debtToEquity, 2);
|
||||
if (m.quickRatio != null) display['Quick'] = fmt(m.quickRatio, 2);
|
||||
if (m.beta != null) display['Beta'] = fmt(m.beta, 2);
|
||||
|
||||
// 52-week movement
|
||||
if (w52pos != null) display['52W Pos'] = w52pos;
|
||||
if (m.week52Change != null) display['52W Chg'] = fmtSign(m.week52Change, '%');
|
||||
if (m.week52FromHigh != null) display['From High'] = fmtSign(m.week52FromHigh, '%');
|
||||
if (m.week52FromLow != null) display['From Low'] = fmtSign(m.week52FromLow, '%');
|
||||
|
||||
// REIT-specific
|
||||
if (m.pFFO != null) display['P/FFO'] = fmt(m.pFFO, 1);
|
||||
|
||||
// Analyst consensus
|
||||
if (m.analystRating != null) {
|
||||
display['Analyst'] = analystLabel(m.analystRating);
|
||||
display['# Analysts'] = m.numberOfAnalysts != null ? String(m.numberOfAnalysts) : null;
|
||||
display['Target'] =
|
||||
m.analystTargetPrice != null ? this.formatCurrency(m.analystTargetPrice) : null;
|
||||
display['Upside'] = fmtSign(m.analystUpside, '%');
|
||||
}
|
||||
|
||||
// DCF
|
||||
if (m.dcfIntrinsicValue != null) {
|
||||
display['DCF Value'] = this.formatCurrency(m.dcfIntrinsicValue);
|
||||
display['DCF Safety'] =
|
||||
m.dcfMarginOfSafety != null ? fmtSign(m.dcfMarginOfSafety, '%') : null;
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user