feat(fundamentals): Add FundamentalsService and SEC EDGAR provider with caching, rate-limiting, tests, and docs updates
This commit is contained in:
287
test/test.fundamentals.service.node.ts
Normal file
287
test/test.fundamentals.service.node.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as opendata from '../ts/index.js';
|
||||
|
||||
const TEST_USER_AGENT = 'fin.cx test@fin.cx';
|
||||
|
||||
tap.test('FundamentalsService - Provider Registration', async () => {
|
||||
const service = new opendata.FundamentalsService();
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
await tap.test('should register provider', async () => {
|
||||
service.register(provider);
|
||||
|
||||
const registered = service.getProvider('SEC EDGAR');
|
||||
expect(registered).toBeDefined();
|
||||
expect(registered?.name).toEqual('SEC EDGAR');
|
||||
});
|
||||
|
||||
await tap.test('should get all providers', async () => {
|
||||
const providers = service.getAllProviders();
|
||||
expect(providers.length).toBeGreaterThan(0);
|
||||
expect(providers[0].name).toEqual('SEC EDGAR');
|
||||
});
|
||||
|
||||
await tap.test('should get enabled providers', async () => {
|
||||
const providers = service.getEnabledProviders();
|
||||
expect(providers.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await tap.test('should unregister provider', async () => {
|
||||
service.unregister('SEC EDGAR');
|
||||
|
||||
const registered = service.getProvider('SEC EDGAR');
|
||||
expect(registered).toBeUndefined();
|
||||
|
||||
// Re-register for other tests
|
||||
service.register(provider);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('FundamentalsService - Fetch Fundamentals', async () => {
|
||||
const service = new opendata.FundamentalsService();
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
service.register(provider);
|
||||
|
||||
await tap.test('should fetch fundamentals for single ticker', async () => {
|
||||
const fundamentals = await service.getFundamentals('AAPL');
|
||||
|
||||
expect(fundamentals).toBeDefined();
|
||||
expect(fundamentals.ticker).toEqual('AAPL');
|
||||
expect(fundamentals.companyName).toEqual('Apple Inc.');
|
||||
expect(fundamentals.provider).toEqual('SEC EDGAR');
|
||||
expect(fundamentals.earningsPerShareDiluted).toBeGreaterThan(0);
|
||||
expect(fundamentals.sharesOutstanding).toBeGreaterThan(0);
|
||||
|
||||
console.log('\n📊 Fetched via Service:');
|
||||
console.log(` ${fundamentals.ticker}: ${fundamentals.companyName}`);
|
||||
console.log(` EPS: $${fundamentals.earningsPerShareDiluted?.toFixed(2)}`);
|
||||
console.log(` Shares: ${(fundamentals.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`);
|
||||
});
|
||||
|
||||
await tap.test('should fetch fundamentals for multiple tickers', async () => {
|
||||
const fundamentalsList = await service.getBatchFundamentals(['AAPL', 'MSFT']);
|
||||
|
||||
expect(fundamentalsList).toBeInstanceOf(Array);
|
||||
expect(fundamentalsList.length).toEqual(2);
|
||||
|
||||
const apple = fundamentalsList.find(f => f.ticker === 'AAPL');
|
||||
const msft = fundamentalsList.find(f => f.ticker === 'MSFT');
|
||||
|
||||
expect(apple).toBeDefined();
|
||||
expect(msft).toBeDefined();
|
||||
expect(apple!.companyName).toEqual('Apple Inc.');
|
||||
expect(msft!.companyName).toContain('Microsoft');
|
||||
|
||||
console.log('\n📊 Batch Fetch via Service:');
|
||||
fundamentalsList.forEach(f => {
|
||||
console.log(` ${f.ticker}: ${f.companyName} - EPS: $${f.earningsPerShareDiluted?.toFixed(2)}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('FundamentalsService - Caching', async () => {
|
||||
const service = new opendata.FundamentalsService({
|
||||
ttl: 60000, // 60 seconds for testing
|
||||
maxEntries: 100
|
||||
});
|
||||
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
service.register(provider);
|
||||
|
||||
await tap.test('should cache fundamentals data', async () => {
|
||||
// Clear cache first
|
||||
service.clearCache();
|
||||
|
||||
let stats = service.getCacheStats();
|
||||
expect(stats.size).toEqual(0);
|
||||
|
||||
// First fetch (should hit API)
|
||||
const start1 = Date.now();
|
||||
await service.getFundamentals('AAPL');
|
||||
const duration1 = Date.now() - start1;
|
||||
|
||||
stats = service.getCacheStats();
|
||||
expect(stats.size).toEqual(1);
|
||||
|
||||
// Second fetch (should hit cache - much faster)
|
||||
const start2 = Date.now();
|
||||
await service.getFundamentals('AAPL');
|
||||
const duration2 = Date.now() - start2;
|
||||
|
||||
expect(duration2).toBeLessThan(duration1);
|
||||
|
||||
console.log('\n⚡ Cache Performance:');
|
||||
console.log(` First fetch: ${duration1}ms`);
|
||||
console.log(` Cached fetch: ${duration2}ms`);
|
||||
console.log(` Speedup: ${Math.round(duration1 / duration2)}x`);
|
||||
});
|
||||
|
||||
await tap.test('should respect cache TTL', async () => {
|
||||
// Set very short TTL
|
||||
service.setCacheTTL(100); // 100ms
|
||||
|
||||
// Fetch and cache
|
||||
await service.getFundamentals('MSFT');
|
||||
|
||||
// Wait for TTL to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
// This should fetch again (cache expired)
|
||||
const stats = service.getCacheStats();
|
||||
console.log(`\n⏱️ Cache TTL: ${stats.ttl}ms`);
|
||||
});
|
||||
|
||||
await tap.test('should clear cache', async () => {
|
||||
service.clearCache();
|
||||
|
||||
const stats = service.getCacheStats();
|
||||
expect(stats.size).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('FundamentalsService - Price Enrichment', async () => {
|
||||
const service = new opendata.FundamentalsService();
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
service.register(provider);
|
||||
|
||||
await tap.test('should enrich fundamentals with price to calculate market cap', async () => {
|
||||
const fundamentals = await service.getFundamentals('AAPL');
|
||||
|
||||
// Simulate current price
|
||||
const currentPrice = 270.37;
|
||||
|
||||
const enriched = await service.enrichWithPrice(fundamentals, currentPrice);
|
||||
|
||||
expect(enriched.marketCap).toBeDefined();
|
||||
expect(enriched.priceToEarnings).toBeDefined();
|
||||
expect(enriched.priceToBook).toBeDefined();
|
||||
|
||||
expect(enriched.marketCap).toBeGreaterThan(0);
|
||||
expect(enriched.priceToEarnings).toBeGreaterThan(0);
|
||||
|
||||
console.log('\n💰 Enriched with Price ($270.37):');
|
||||
console.log(` Market Cap: $${(enriched.marketCap! / 1_000_000_000_000).toFixed(2)}T`);
|
||||
console.log(` P/E Ratio: ${enriched.priceToEarnings!.toFixed(2)}`);
|
||||
console.log(` P/B Ratio: ${enriched.priceToBook?.toFixed(2) || 'N/A'}`);
|
||||
|
||||
// Verify calculations
|
||||
const expectedMarketCap = fundamentals.sharesOutstanding! * currentPrice;
|
||||
expect(Math.abs(enriched.marketCap! - expectedMarketCap)).toBeLessThan(1); // Allow for rounding
|
||||
|
||||
const expectedPE = currentPrice / fundamentals.earningsPerShareDiluted!;
|
||||
expect(Math.abs(enriched.priceToEarnings! - expectedPE)).toBeLessThan(0.01);
|
||||
});
|
||||
|
||||
await tap.test('should enrich batch fundamentals with prices', async () => {
|
||||
const fundamentalsList = await service.getBatchFundamentals(['AAPL', 'MSFT']);
|
||||
|
||||
const priceMap = new Map<string, number>([
|
||||
['AAPL', 270.37],
|
||||
['MSFT', 425.50]
|
||||
]);
|
||||
|
||||
const enriched = await service.enrichBatchWithPrices(fundamentalsList, priceMap);
|
||||
|
||||
expect(enriched.length).toEqual(2);
|
||||
|
||||
const apple = enriched.find(f => f.ticker === 'AAPL')!;
|
||||
const msft = enriched.find(f => f.ticker === 'MSFT')!;
|
||||
|
||||
expect(apple.marketCap).toBeGreaterThan(0);
|
||||
expect(msft.marketCap).toBeGreaterThan(0);
|
||||
|
||||
console.log('\n💰 Batch Enrichment:');
|
||||
console.log(` AAPL: Market Cap $${(apple.marketCap! / 1_000_000_000_000).toFixed(2)}T, P/E ${apple.priceToEarnings!.toFixed(2)}`);
|
||||
console.log(` MSFT: Market Cap $${(msft.marketCap! / 1_000_000_000_000).toFixed(2)}T, P/E ${msft.priceToEarnings!.toFixed(2)}`);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('FundamentalsService - Provider Health', async () => {
|
||||
const service = new opendata.FundamentalsService();
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
service.register(provider);
|
||||
|
||||
await tap.test('should check provider health', async () => {
|
||||
const health = await service.checkProvidersHealth();
|
||||
|
||||
expect(health.size).toEqual(1);
|
||||
expect(health.get('SEC EDGAR')).toBe(true);
|
||||
|
||||
console.log('\n💚 Provider Health:');
|
||||
health.forEach((isHealthy, name) => {
|
||||
console.log(` ${name}: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('FundamentalsService - Provider Statistics', async () => {
|
||||
const service = new opendata.FundamentalsService();
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
service.register(provider);
|
||||
|
||||
await tap.test('should track provider statistics', async () => {
|
||||
// Make some requests
|
||||
await service.getFundamentals('AAPL');
|
||||
await service.getFundamentals('MSFT');
|
||||
|
||||
const stats = service.getProviderStats();
|
||||
|
||||
expect(stats.size).toEqual(1);
|
||||
|
||||
const secStats = stats.get('SEC EDGAR');
|
||||
expect(secStats).toBeDefined();
|
||||
expect(secStats!.successCount).toBeGreaterThan(0);
|
||||
|
||||
console.log('\n📈 Provider Stats:');
|
||||
console.log(` Success Count: ${secStats!.successCount}`);
|
||||
console.log(` Error Count: ${secStats!.errorCount}`);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('FundamentalsService - Error Handling', async () => {
|
||||
const service = new opendata.FundamentalsService();
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
service.register(provider);
|
||||
|
||||
await tap.test('should throw error for invalid ticker', async () => {
|
||||
try {
|
||||
await service.getFundamentals('INVALIDTICKER123456');
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('CIK not found');
|
||||
}
|
||||
});
|
||||
|
||||
await tap.test('should throw error when no providers available', async () => {
|
||||
const emptyService = new opendata.FundamentalsService();
|
||||
|
||||
try {
|
||||
await emptyService.getFundamentals('AAPL');
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('No fundamentals providers available');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
261
test/test.secedgar.provider.node.ts
Normal file
261
test/test.secedgar.provider.node.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as opendata from '../ts/index.js';
|
||||
|
||||
// Test configuration
|
||||
const TEST_USER_AGENT = 'fin.cx test@fin.cx';
|
||||
const TEST_TICKER = 'AAPL'; // Apple Inc - well-known test case
|
||||
const RATE_LIMIT_DELAY = 150; // 150ms between requests (< 10 req/sec)
|
||||
|
||||
tap.test('SEC EDGAR Provider - Constructor', async () => {
|
||||
await tap.test('should create provider with valid User-Agent', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
expect(provider.name).toEqual('SEC EDGAR');
|
||||
expect(provider.priority).toEqual(100);
|
||||
expect(provider.requiresAuth).toBe(false);
|
||||
expect(provider.rateLimit?.requestsPerMinute).toEqual(600);
|
||||
});
|
||||
|
||||
await tap.test('should throw error if User-Agent is missing', async () => {
|
||||
expect(() => {
|
||||
new opendata.SecEdgarProvider({
|
||||
userAgent: ''
|
||||
});
|
||||
}).toThrow('User-Agent is required');
|
||||
});
|
||||
|
||||
await tap.test('should throw error if User-Agent format is invalid', async () => {
|
||||
expect(() => {
|
||||
new opendata.SecEdgarProvider({
|
||||
userAgent: 'InvalidFormat'
|
||||
});
|
||||
}).toThrow('Invalid User-Agent format');
|
||||
});
|
||||
|
||||
await tap.test('should accept valid User-Agent with space and email', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: 'MyCompany contact@example.com'
|
||||
});
|
||||
|
||||
expect(provider).toBeInstanceOf(opendata.SecEdgarProvider);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('SEC EDGAR Provider - Availability', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
await tap.test('should report as available', async () => {
|
||||
const isAvailable = await provider.isAvailable();
|
||||
expect(isAvailable).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('SEC EDGAR Provider - Fetch Fundamentals', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
await tap.test('should fetch fundamentals for Apple (AAPL)', async () => {
|
||||
const fundamentals = await provider.fetchData({
|
||||
type: 'fundamentals-current',
|
||||
ticker: TEST_TICKER
|
||||
});
|
||||
|
||||
// Verify structure
|
||||
expect(fundamentals).toBeDefined();
|
||||
expect(fundamentals).not.toBeInstanceOf(Array);
|
||||
|
||||
const data = fundamentals as opendata.IStockFundamentals;
|
||||
|
||||
// Basic fields
|
||||
expect(data.ticker).toEqual('AAPL');
|
||||
expect(data.cik).toBeDefined();
|
||||
expect(data.companyName).toEqual('Apple Inc.');
|
||||
expect(data.provider).toEqual('SEC EDGAR');
|
||||
expect(data.timestamp).toBeInstanceOf(Date);
|
||||
expect(data.fetchedAt).toBeInstanceOf(Date);
|
||||
|
||||
// Financial metrics (Apple should have all of these)
|
||||
expect(data.earningsPerShareDiluted).toBeDefined();
|
||||
expect(data.earningsPerShareDiluted).toBeGreaterThan(0);
|
||||
|
||||
expect(data.sharesOutstanding).toBeDefined();
|
||||
expect(data.sharesOutstanding).toBeGreaterThan(0);
|
||||
|
||||
expect(data.revenue).toBeDefined();
|
||||
expect(data.revenue).toBeGreaterThan(0);
|
||||
|
||||
expect(data.netIncome).toBeDefined();
|
||||
expect(data.assets).toBeDefined();
|
||||
expect(data.assets).toBeGreaterThan(0);
|
||||
|
||||
expect(data.liabilities).toBeDefined();
|
||||
expect(data.stockholdersEquity).toBeDefined();
|
||||
|
||||
// Metadata
|
||||
expect(data.fiscalYear).toBeDefined();
|
||||
|
||||
console.log('\n📊 Sample Apple Fundamentals:');
|
||||
console.log(` Company: ${data.companyName} (CIK: ${data.cik})`);
|
||||
console.log(` EPS (Diluted): $${data.earningsPerShareDiluted?.toFixed(2)}`);
|
||||
console.log(` Shares Outstanding: ${(data.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`);
|
||||
console.log(` Revenue: $${(data.revenue! / 1_000_000_000).toFixed(2)}B`);
|
||||
console.log(` Net Income: $${(data.netIncome! / 1_000_000_000).toFixed(2)}B`);
|
||||
console.log(` Assets: $${(data.assets! / 1_000_000_000).toFixed(2)}B`);
|
||||
console.log(` Fiscal Year: ${data.fiscalYear}`);
|
||||
});
|
||||
|
||||
await tap.test('should throw error for invalid ticker', async () => {
|
||||
try {
|
||||
await provider.fetchData({
|
||||
type: 'fundamentals-current',
|
||||
ticker: 'INVALIDTICKER123456'
|
||||
});
|
||||
throw new Error('Should have thrown error');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('CIK not found');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('SEC EDGAR Provider - Batch Fetch', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
await tap.test('should fetch fundamentals for multiple tickers', async () => {
|
||||
const result = await provider.fetchData({
|
||||
type: 'fundamentals-batch',
|
||||
tickers: ['AAPL', 'MSFT']
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(Array);
|
||||
|
||||
const fundamentalsList = result as opendata.IStockFundamentals[];
|
||||
expect(fundamentalsList.length).toEqual(2);
|
||||
|
||||
// Check Apple
|
||||
const apple = fundamentalsList.find(f => f.ticker === 'AAPL');
|
||||
expect(apple).toBeDefined();
|
||||
expect(apple!.companyName).toEqual('Apple Inc.');
|
||||
expect(apple!.earningsPerShareDiluted).toBeGreaterThan(0);
|
||||
|
||||
// Check Microsoft
|
||||
const microsoft = fundamentalsList.find(f => f.ticker === 'MSFT');
|
||||
expect(microsoft).toBeDefined();
|
||||
expect(microsoft!.companyName).toContain('Microsoft');
|
||||
expect(microsoft!.earningsPerShareDiluted).toBeGreaterThan(0);
|
||||
|
||||
console.log('\n📊 Batch Fetch Results:');
|
||||
fundamentalsList.forEach(f => {
|
||||
console.log(` ${f.ticker}: ${f.companyName} - EPS: $${f.earningsPerShareDiluted?.toFixed(2)}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('SEC EDGAR Provider - CIK Caching', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
await tap.test('should cache CIK lookups', async () => {
|
||||
// Clear cache first
|
||||
provider.clearCache();
|
||||
|
||||
let stats = provider.getCacheStats();
|
||||
expect(stats.cikCacheSize).toEqual(0);
|
||||
|
||||
// First fetch (should populate cache)
|
||||
await provider.fetchData({
|
||||
type: 'fundamentals-current',
|
||||
ticker: 'AAPL'
|
||||
});
|
||||
|
||||
stats = provider.getCacheStats();
|
||||
expect(stats.cikCacheSize).toBeGreaterThan(0);
|
||||
|
||||
console.log(`\n💾 CIK Cache: ${stats.cikCacheSize} entries`);
|
||||
});
|
||||
|
||||
await tap.test('should clear cache', async () => {
|
||||
provider.clearCache();
|
||||
|
||||
const stats = provider.getCacheStats();
|
||||
expect(stats.cikCacheSize).toEqual(0);
|
||||
expect(stats.hasTickerList).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('SEC EDGAR Provider - Rate Limiting', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
await tap.test('should handle multiple rapid requests without exceeding rate limit', async () => {
|
||||
// Make 5 requests in succession
|
||||
// Rate limiter should ensure we don't exceed 10 req/sec
|
||||
const tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META'];
|
||||
const startTime = Date.now();
|
||||
|
||||
const promises = tickers.map(ticker =>
|
||||
provider.fetchData({
|
||||
type: 'fundamentals-current',
|
||||
ticker
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(results.length).toEqual(5);
|
||||
console.log(`\n⏱️ 5 requests completed in ${duration}ms (avg: ${Math.round(duration / 5)}ms/request)`);
|
||||
|
||||
// Verify all results are valid
|
||||
results.forEach((result, index) => {
|
||||
expect(result).toBeDefined();
|
||||
const data = result as opendata.IStockFundamentals;
|
||||
expect(data.ticker).toEqual(tickers[index]);
|
||||
expect(data.earningsPerShareDiluted).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('SEC EDGAR Provider - Market Cap Calculation', async () => {
|
||||
const provider = new opendata.SecEdgarProvider({
|
||||
userAgent: TEST_USER_AGENT
|
||||
});
|
||||
|
||||
await tap.test('should provide data needed for market cap calculation', async () => {
|
||||
const fundamentals = await provider.fetchData({
|
||||
type: 'fundamentals-current',
|
||||
ticker: 'AAPL'
|
||||
}) as opendata.IStockFundamentals;
|
||||
|
||||
expect(fundamentals.sharesOutstanding).toBeDefined();
|
||||
expect(fundamentals.earningsPerShareDiluted).toBeDefined();
|
||||
|
||||
// Simulate current price (in real usage, this comes from price provider)
|
||||
const simulatedPrice = 270.37;
|
||||
|
||||
// Calculate market cap
|
||||
const marketCap = fundamentals.sharesOutstanding! * simulatedPrice;
|
||||
const pe = simulatedPrice / fundamentals.earningsPerShareDiluted!;
|
||||
|
||||
console.log('\n💰 Calculated Metrics (with simulated price $270.37):');
|
||||
console.log(` Shares Outstanding: ${(fundamentals.sharesOutstanding! / 1_000_000_000).toFixed(2)}B`);
|
||||
console.log(` Market Cap: $${(marketCap / 1_000_000_000_000).toFixed(2)}T`);
|
||||
console.log(` EPS: $${fundamentals.earningsPerShareDiluted!.toFixed(2)}`);
|
||||
console.log(` P/E Ratio: ${pe.toFixed(2)}`);
|
||||
|
||||
expect(marketCap).toBeGreaterThan(0);
|
||||
expect(pe).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user