@@ -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 >= 200 e9 ) return CAP_CATEGORY . MEGA ;
if ( marketCap >= 10 e9 ) return CAP_CATEGORY . LARGE ;
if ( marketCap >= 2 e9 ) return CAP_CATEGORY . MID ;
if ( marketCap >= 300 e6 ) 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 ;
}
}