phase-6: typescript introduction
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
export class Asset {
|
||||
constructor(data) {
|
||||
this.ticker = (data.ticker || 'UNKNOWN').toUpperCase();
|
||||
this.currentPrice = data.currentPrice || 0;
|
||||
this.type = (data.type || 'STOCK').toUpperCase();
|
||||
}
|
||||
|
||||
formatCurrency(val) {
|
||||
return val ? `$${val.toFixed(2)}` : 'N/A';
|
||||
}
|
||||
|
||||
formatLargeNumber(num) {
|
||||
if (!num) return 'N/A';
|
||||
if (num >= 1e12) return `${(num / 1e12).toFixed(2)}T`;
|
||||
if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
|
||||
if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { AssetType } from '../../types.js';
|
||||
|
||||
interface AssetData {
|
||||
ticker?: string;
|
||||
currentPrice?: number;
|
||||
type?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export class Asset {
|
||||
ticker: string;
|
||||
currentPrice: number;
|
||||
type: AssetType;
|
||||
|
||||
constructor(data: AssetData) {
|
||||
this.ticker = (data.ticker || 'UNKNOWN').toUpperCase();
|
||||
this.currentPrice = (data.currentPrice as number) || 0;
|
||||
this.type = (data.type || 'STOCK').toUpperCase() as AssetType;
|
||||
}
|
||||
|
||||
formatCurrency(val: number | null | undefined): string {
|
||||
return val ? `$${val.toFixed(2)}` : 'N/A';
|
||||
}
|
||||
|
||||
formatLargeNumber(num: number | null | undefined): string {
|
||||
if (!num) return 'N/A';
|
||||
if (num >= 1e12) return `${(num / 1e12).toFixed(2)}T`;
|
||||
if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
|
||||
if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
|
||||
return num.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,40 @@
|
||||
import { CREDIT_RATING_SCALE } from '../../config/ScoringConfig.js';
|
||||
import { Asset } from './Asset.js';
|
||||
|
||||
interface BondData {
|
||||
ticker?: string;
|
||||
currentPrice?: number;
|
||||
creditRating?: string;
|
||||
yieldToMaturity?: string | number;
|
||||
duration?: string | number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface BondMetrics {
|
||||
ytm: number;
|
||||
duration: number;
|
||||
creditRating: string;
|
||||
creditRatingNumeric: number;
|
||||
}
|
||||
|
||||
export class Bond extends Asset {
|
||||
constructor(data) {
|
||||
metrics: BondMetrics;
|
||||
|
||||
constructor(data: BondData) {
|
||||
super(data);
|
||||
|
||||
const creditRating = data.creditRating || 'BBB';
|
||||
const creditRatingNumeric = CREDIT_RATING_SCALE[creditRating] ?? 7;
|
||||
|
||||
this.metrics = {
|
||||
ytm: parseFloat(data.yieldToMaturity) || 0,
|
||||
duration: parseFloat(data.duration) || 0,
|
||||
ytm: parseFloat(String(data.yieldToMaturity)) || 0,
|
||||
duration: parseFloat(String(data.duration)) || 0,
|
||||
creditRating,
|
||||
creditRatingNumeric,
|
||||
};
|
||||
}
|
||||
|
||||
getDisplayMetrics() {
|
||||
getDisplayMetrics(): Record<string, string> {
|
||||
return {
|
||||
Ticker: this.ticker,
|
||||
Type: 'BOND',
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Asset } from './Asset.js';
|
||||
|
||||
export class Etf extends Asset {
|
||||
constructor(data) {
|
||||
super(data);
|
||||
this.metrics = {
|
||||
expenseRatio: parseFloat(data.expenseRatio) || 0,
|
||||
totalAssets: parseFloat(data.totalAssets) || 0,
|
||||
yield: parseFloat(data.yield) || 0,
|
||||
volume: parseFloat(data.volume) || 0,
|
||||
fiveYearReturn: parseFloat(data.fiveYearReturn) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
getDisplayMetrics() {
|
||||
return {
|
||||
Ticker: this.ticker,
|
||||
Type: 'ETF',
|
||||
Price: this.formatCurrency(this.currentPrice),
|
||||
'Exp Ratio%': `${this.metrics.expenseRatio.toFixed(2)}%`,
|
||||
'Yield%': `${this.metrics.yield.toFixed(2)}%`,
|
||||
AUM: this.formatLargeNumber(this.metrics.totalAssets),
|
||||
'5Y Return%': `${this.metrics.fiveYearReturn.toFixed(1)}%`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Asset } from './Asset.js';
|
||||
|
||||
interface EtfData {
|
||||
ticker?: string;
|
||||
currentPrice?: number;
|
||||
expenseRatio?: string | number;
|
||||
totalAssets?: string | number;
|
||||
yield?: string | number;
|
||||
volume?: string | number;
|
||||
fiveYearReturn?: string | number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface EtfMetrics {
|
||||
expenseRatio: number;
|
||||
totalAssets: number;
|
||||
yield: number;
|
||||
volume: number;
|
||||
fiveYearReturn: number;
|
||||
}
|
||||
|
||||
export class Etf extends Asset {
|
||||
metrics: EtfMetrics;
|
||||
|
||||
constructor(data: EtfData) {
|
||||
super(data);
|
||||
this.metrics = {
|
||||
expenseRatio: parseFloat(String(data.expenseRatio)) || 0,
|
||||
totalAssets: parseFloat(String(data.totalAssets)) || 0,
|
||||
yield: parseFloat(String(data.yield)) || 0,
|
||||
volume: parseFloat(String(data.volume)) || 0,
|
||||
fiveYearReturn: parseFloat(String(data.fiveYearReturn)) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
getDisplayMetrics(): Record<string, string> {
|
||||
return {
|
||||
Ticker: this.ticker,
|
||||
Type: 'ETF',
|
||||
Price: this.formatCurrency(this.currentPrice),
|
||||
'Exp Ratio%': `${this.metrics.expenseRatio.toFixed(2)}%`,
|
||||
'Yield%': `${this.metrics.yield.toFixed(2)}%`,
|
||||
AUM: this.formatLargeNumber(this.metrics.totalAssets),
|
||||
'5Y Return%': `${this.metrics.fiveYearReturn.toFixed(1)}%`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,86 @@
|
||||
import { Asset } from './Asset.js';
|
||||
import type { Sector } from '../../config/constants.js';
|
||||
|
||||
interface StockData {
|
||||
ticker?: string;
|
||||
currentPrice?: number;
|
||||
assetProfile?: { industry?: string; sector?: string };
|
||||
peRatio?: number | null;
|
||||
pegRatio?: number | null;
|
||||
priceToBook?: number | null;
|
||||
netProfitMargin?: number | null;
|
||||
operatingMargin?: number | null;
|
||||
returnOnEquity?: number | null;
|
||||
revenueGrowth?: number | null;
|
||||
earningsGrowth?: number | null;
|
||||
debtToEquity?: number | null;
|
||||
quickRatio?: number | null;
|
||||
fcfYield?: number | null;
|
||||
pFFO?: number | null;
|
||||
dividendYield?: number | null;
|
||||
beta?: number | null;
|
||||
week52High?: number | null;
|
||||
week52Low?: number | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface StockMetrics {
|
||||
sector: Sector;
|
||||
peRatio: number | null;
|
||||
pegRatio: number | null;
|
||||
priceToBook: number | null;
|
||||
netProfitMargin: number | null;
|
||||
operatingMargin: number | null;
|
||||
returnOnEquity: number | null;
|
||||
revenueGrowth: number | null;
|
||||
earningsGrowth: number | null;
|
||||
debtToEquity: number | null;
|
||||
quickRatio: number | null;
|
||||
fcfYield: number | null;
|
||||
pFFO: number | null;
|
||||
dividendYield: number | null;
|
||||
beta: number | null;
|
||||
week52High: number | null;
|
||||
week52Low: number | null;
|
||||
currentPrice: number;
|
||||
}
|
||||
|
||||
export class Stock extends Asset {
|
||||
constructor(data) {
|
||||
sector: Sector;
|
||||
metrics: StockMetrics;
|
||||
|
||||
constructor(data: StockData) {
|
||||
super(data);
|
||||
// console.log('Data:', data);
|
||||
this.sector = this._mapToStandardSector(data || {});
|
||||
this.sector = this._mapToStandardSector(data);
|
||||
|
||||
this.metrics = {
|
||||
sector: this.sector,
|
||||
// Valuation
|
||||
peRatio: data.peRatio ?? null,
|
||||
pegRatio: data.pegRatio ?? null,
|
||||
priceToBook: data.priceToBook ?? null,
|
||||
// Profitability
|
||||
netProfitMargin: data.netProfitMargin ?? null,
|
||||
operatingMargin: data.operatingMargin ?? null,
|
||||
returnOnEquity: data.returnOnEquity ?? null,
|
||||
// Growth
|
||||
revenueGrowth: data.revenueGrowth ?? null,
|
||||
earningsGrowth: data.earningsGrowth ?? null,
|
||||
// Financial health
|
||||
debtToEquity: data.debtToEquity ?? null,
|
||||
quickRatio: data.quickRatio ?? null,
|
||||
// Cash flow
|
||||
fcfYield: data.fcfYield ?? null,
|
||||
pFFO: data.pFFO ?? null,
|
||||
// Income
|
||||
dividendYield: data.dividendYield ?? null,
|
||||
// Risk & momentum
|
||||
beta: data.beta ?? null,
|
||||
week52High: data.week52High ?? null,
|
||||
week52Low: data.week52Low ?? null,
|
||||
currentPrice: data.currentPrice ?? 0,
|
||||
currentPrice: (data.currentPrice as number) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
_mapToStandardSector(data) {
|
||||
const profile = data.assetProfile || {};
|
||||
_mapToStandardSector(data: StockData): Sector {
|
||||
const profile = data.assetProfile ?? {};
|
||||
const industry = (profile.industry || '').toLowerCase();
|
||||
const sector = (profile.sector || '').toLowerCase();
|
||||
const combined = `${industry} ${sector}`;
|
||||
|
||||
// Yahoo Finance sector/industry strings mapped to our internal sector constants.
|
||||
// Order matters — more specific matches first.
|
||||
if (
|
||||
combined.includes('technology') ||
|
||||
combined.includes('electronic') ||
|
||||
@@ -72,7 +110,6 @@ export class Stock extends Asset {
|
||||
combined.includes('medical')
|
||||
)
|
||||
return 'HEALTHCARE';
|
||||
// Yahoo calls this "Communication Services" — covers META, GOOGL, NFLX, DIS, T
|
||||
if (
|
||||
combined.includes('communication') ||
|
||||
combined.includes('media') ||
|
||||
@@ -100,20 +137,22 @@ export class Stock extends Asset {
|
||||
return 'GENERAL';
|
||||
}
|
||||
|
||||
getDisplayMetrics() {
|
||||
const fmt = (v, dec = 1, suffix = '') => (v != null ? `${v.toFixed(dec)}${suffix}` : null);
|
||||
getDisplayMetrics(): Record<string, string | null> {
|
||||
const fmt = (v: number | null, dec = 1, suffix = '') =>
|
||||
v != null ? `${v.toFixed(dec)}${suffix}` : null;
|
||||
const m = this.metrics;
|
||||
|
||||
const w52pos =
|
||||
m.week52High > 0 && m.week52Low != null && m.currentPrice > 0
|
||||
m.week52High != null && m.week52High > 0 && m.week52Low != null && m.currentPrice > 0
|
||||
? (((m.currentPrice - m.week52Low) / (m.week52High - m.week52Low)) * 100).toFixed(0) + '%'
|
||||
: null;
|
||||
|
||||
// Only include fields that have actual data — null fields are omitted
|
||||
const display = {
|
||||
const display: Record<string, string | null> = {
|
||||
Ticker: this.ticker,
|
||||
Price: this.formatCurrency(this.currentPrice),
|
||||
Sector: this.sector,
|
||||
};
|
||||
|
||||
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);
|
||||
Reference in New Issue
Block a user