150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DataMapper } from '../server/services/DataMapper';
|
|
|
|
const base = {
|
|
price: { quoteType: 'EQUITY', regularMarketPrice: 150 },
|
|
assetProfile: { sector: 'Technology', industry: 'Software', category: '' },
|
|
financialData: {
|
|
quickRatio: 1.2,
|
|
debtToEquity: 150,
|
|
freeCashflow: 5e9,
|
|
revenueGrowth: 0.15,
|
|
profitMargins: 0.25,
|
|
operatingMargins: 0.3,
|
|
returnOnEquity: 0.2,
|
|
earningsGrowth: 0.12,
|
|
operatingCashflow: 8e9,
|
|
},
|
|
defaultKeyStatistics: { pegRatio: null, forwardPE: 28, sharesOutstanding: 1e9, priceToBook: 12 },
|
|
summaryDetail: {
|
|
trailingAnnualDividendYield: 0.005,
|
|
trailingPE: 30,
|
|
beta: 1.2,
|
|
fiftyTwoWeekHigh: 200,
|
|
fiftyTwoWeekLow: 120,
|
|
},
|
|
};
|
|
|
|
test('maps EQUITY quote type to STOCK', () => {
|
|
const result = DataMapper.mapToStandardFormat('AAPL', base);
|
|
assert.equal(result.type, 'STOCK');
|
|
assert.equal(result.ticker, 'AAPL');
|
|
});
|
|
|
|
test('computes PEG from trailingPE / earningsGrowth when Yahoo returns null', () => {
|
|
const result = DataMapper.mapToStandardFormat('AAPL', base);
|
|
const expected = +(30 / (0.12 * 100)).toFixed(2); // trailingPE=30, earningsGrowth=12%
|
|
assert.equal(result.pegRatio, expected);
|
|
});
|
|
|
|
test('uses Yahoo pegRatio when available', () => {
|
|
const summary = {
|
|
...base,
|
|
defaultKeyStatistics: { ...base.defaultKeyStatistics, pegRatio: 1.5 },
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('AAPL', summary);
|
|
assert.equal(result.pegRatio, 1.5);
|
|
});
|
|
|
|
test('debtToEquity is divided by 100', () => {
|
|
const result = DataMapper.mapToStandardFormat('AAPL', base);
|
|
assert.equal(result.debtToEquity, 1.5); // 150 / 100
|
|
});
|
|
|
|
test('maps ETF quoteType to ETF', () => {
|
|
const etfSummary = {
|
|
...base,
|
|
price: { ...base.price, quoteType: 'ETF' },
|
|
assetProfile: { category: 'Large Blend' },
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('VOO', etfSummary);
|
|
assert.equal(result.type, 'ETF');
|
|
});
|
|
|
|
test('classifies bond ETF from category keyword', () => {
|
|
const bondSummary = {
|
|
...base,
|
|
price: { ...base.price, quoteType: 'ETF' },
|
|
assetProfile: { category: 'Intermediate-Term Bond' },
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('BND', bondSummary);
|
|
assert.equal(result.type, 'BOND');
|
|
});
|
|
|
|
test('FCF yield is computed when data available', () => {
|
|
const result = DataMapper.mapToStandardFormat('AAPL', base);
|
|
assert.notEqual(result.fcfYield, null);
|
|
assert((result.fcfYield as number) > 0);
|
|
});
|
|
|
|
test('peRatio prefers trailingPE over forwardPE', () => {
|
|
// trailingPE=30 in summaryDetail, forwardPE=28 in defaultKeyStatistics
|
|
const result = DataMapper.mapToStandardFormat('AAPL', base);
|
|
assert.equal(result.peRatio, 30); // trailing should win
|
|
});
|
|
|
|
test('negative FCF yield is preserved, not nulled', () => {
|
|
const negativeFcf = {
|
|
...base,
|
|
financialData: { ...base.financialData, freeCashflow: -2e9 },
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('AAPL', negativeFcf);
|
|
assert.notEqual(result.fcfYield, null);
|
|
assert((result.fcfYield as number) < 0, 'negative FCF should produce negative yield, not null');
|
|
});
|
|
|
|
test('ETF maps volume from summaryDetail', () => {
|
|
const etfSummary = {
|
|
...base,
|
|
price: { ...base.price, quoteType: 'ETF' },
|
|
assetProfile: { category: 'Large Blend' },
|
|
summaryDetail: {
|
|
...base.summaryDetail,
|
|
averageVolume: 5000000,
|
|
expenseRatio: 0.0003,
|
|
trailingAnnualDividendYield: 0.013,
|
|
},
|
|
defaultKeyStatistics: { fiveYearAverageReturn: 0.12 },
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('VOO', etfSummary);
|
|
assert.equal(result.volume, 5000000);
|
|
});
|
|
|
|
test('bond duration inferred from category — intermediate maps to 5y', () => {
|
|
const bondSummary = {
|
|
...base,
|
|
price: { ...base.price, quoteType: 'ETF' },
|
|
assetProfile: { category: 'Intermediate-Term Bond' },
|
|
summaryDetail: { yield: 0.045 },
|
|
defaultKeyStatistics: {},
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('BND', bondSummary);
|
|
assert.equal(result.duration, 5);
|
|
});
|
|
|
|
test('bond duration inferred from category — short-term maps to 2y', () => {
|
|
const bondSummary = {
|
|
...base,
|
|
price: { ...base.price, quoteType: 'ETF' },
|
|
assetProfile: { category: 'Short-Term Bond' },
|
|
summaryDetail: { yield: 0.05 },
|
|
defaultKeyStatistics: {},
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('SHY', bondSummary);
|
|
assert.equal(result.duration, 2);
|
|
});
|
|
|
|
test('metrics are null (not 0) when data missing', () => {
|
|
const sparse = {
|
|
price: { quoteType: 'EQUITY', regularMarketPrice: 100 },
|
|
financialData: {},
|
|
defaultKeyStatistics: {},
|
|
summaryDetail: {},
|
|
assetProfile: {},
|
|
};
|
|
const result = DataMapper.mapToStandardFormat('X', sparse);
|
|
assert.equal(result.pegRatio, null);
|
|
assert.equal(result.quickRatio, null);
|
|
});
|