Files
opendata/test/test.stockdata.service.node+bun+deno.ts

419 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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('StockDataService - Basic Setup', async () => {
await tap.test('should create StockDataService instance', async () => {
const service = new opendata.StockDataService({
cache: {
priceTTL: 60000, // 1 minute for testing
fundamentalsTTL: 120000, // 2 minutes for testing
maxEntries: 100
}
});
expect(service).toBeInstanceOf(opendata.StockDataService);
const stats = service.getCacheStats();
expect(stats.priceCache.ttl).toEqual(60000);
expect(stats.fundamentalsCache.ttl).toEqual(120000);
expect(stats.maxEntries).toEqual(100);
});
});
tap.test('StockDataService - Provider Registration', async () => {
const service = new opendata.StockDataService();
await tap.test('should register price provider', async () => {
const yahooProvider = new opendata.YahooFinanceProvider();
service.registerPriceProvider(yahooProvider);
const providers = service.getPriceProviders();
expect(providers.length).toEqual(1);
expect(providers[0].name).toEqual('Yahoo Finance');
});
await tap.test('should register fundamentals provider', async () => {
const secProvider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
service.registerFundamentalsProvider(secProvider);
const providers = service.getFundamentalsProviders();
expect(providers.length).toEqual(1);
expect(providers[0].name).toEqual('SEC EDGAR');
});
await tap.test('should unregister providers', async () => {
service.unregisterPriceProvider('Yahoo Finance');
service.unregisterFundamentalsProvider('SEC EDGAR');
expect(service.getPriceProviders().length).toEqual(0);
expect(service.getFundamentalsProviders().length).toEqual(0);
});
});
tap.test('StockDataService - Price Fetching', async () => {
const service = new opendata.StockDataService();
const yahooProvider = new opendata.YahooFinanceProvider();
service.registerPriceProvider(yahooProvider);
await tap.test('should fetch single price', async () => {
const price = await service.getPrice('AAPL');
expect(price).toBeDefined();
expect(price.ticker).toEqual('AAPL');
expect(price.price).toBeGreaterThan(0);
expect(price.provider).toEqual('Yahoo Finance');
expect(price.timestamp).toBeInstanceOf(Date);
console.log(`\n💵 Single Price: ${price.ticker} = $${price.price.toFixed(2)}`);
});
await tap.test('should fetch batch prices', async () => {
const prices = await service.getPrices(['AAPL', 'MSFT', 'GOOGL']);
expect(prices).toBeInstanceOf(Array);
expect(prices.length).toBeGreaterThan(0);
expect(prices.length).toBeLessThanOrEqual(3);
console.log('\n💵 Batch Prices:');
prices.forEach(p => {
console.log(` ${p.ticker}: $${p.price.toFixed(2)}`);
});
});
await tap.test('should cache prices', async () => {
// Clear cache
service.clearCache();
const stats1 = service.getCacheStats();
expect(stats1.priceCache.size).toEqual(0);
// Fetch price (should hit API)
const start1 = Date.now();
await service.getPrice('AAPL');
const duration1 = Date.now() - start1;
const stats2 = service.getCacheStats();
expect(stats2.priceCache.size).toEqual(1);
// Fetch again (should hit cache - much faster)
const start2 = Date.now();
await service.getPrice('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`);
});
});
tap.test('StockDataService - Fundamentals Fetching', async () => {
const service = new opendata.StockDataService();
const secProvider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
service.registerFundamentalsProvider(secProvider);
await tap.test('should fetch single fundamentals', 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📊 Single Fundamentals:');
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 batch fundamentals', async () => {
const fundamentals = await service.getBatchFundamentals(['AAPL', 'MSFT']);
expect(fundamentals).toBeInstanceOf(Array);
expect(fundamentals.length).toEqual(2);
console.log('\n📊 Batch Fundamentals:');
fundamentals.forEach(f => {
console.log(` ${f.ticker}: ${f.companyName} - EPS: $${f.earningsPerShareDiluted?.toFixed(2)}`);
});
});
await tap.test('should cache fundamentals', async () => {
// Clear cache
service.clearCache();
const stats1 = service.getCacheStats();
expect(stats1.fundamentalsCache.size).toEqual(0);
// Fetch fundamentals (should hit API)
await service.getFundamentals('AAPL');
const stats2 = service.getCacheStats();
expect(stats2.fundamentalsCache.size).toEqual(1);
});
});
tap.test('StockDataService - Complete Stock Data', async () => {
const service = new opendata.StockDataService();
// Register both providers
const yahooProvider = new opendata.YahooFinanceProvider();
const secProvider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
service.registerPriceProvider(yahooProvider);
service.registerFundamentalsProvider(secProvider);
await tap.test('should fetch complete stock data with string', async () => {
const data = await service.getStockData('AAPL');
expect(data).toBeDefined();
expect(data.ticker).toEqual('AAPL');
expect(data.price).toBeDefined();
expect(data.price.ticker).toEqual('AAPL');
expect(data.fundamentals).toBeDefined();
expect(data.fundamentals?.ticker).toEqual('AAPL');
expect(data.fetchedAt).toBeInstanceOf(Date);
// Check automatic enrichment
expect(data.fundamentals?.marketCap).toBeDefined();
expect(data.fundamentals?.priceToEarnings).toBeDefined();
expect(data.fundamentals?.marketCap).toBeGreaterThan(0);
expect(data.fundamentals?.priceToEarnings).toBeGreaterThan(0);
console.log('\n✨ Complete Stock Data (Auto-Enriched):');
console.log(` ${data.ticker}: ${data.fundamentals?.companyName}`);
console.log(` Price: $${data.price.price.toFixed(2)}`);
console.log(` Market Cap: $${(data.fundamentals!.marketCap! / 1_000_000_000_000).toFixed(2)}T`);
console.log(` P/E Ratio: ${data.fundamentals!.priceToEarnings!.toFixed(2)}`);
});
await tap.test('should fetch complete stock data with request object', async () => {
const data = await service.getStockData({
ticker: 'MSFT',
includeFundamentals: true,
enrichFundamentals: true
});
expect(data).toBeDefined();
expect(data.ticker).toEqual('MSFT');
expect(data.price).toBeDefined();
expect(data.fundamentals).toBeDefined();
expect(data.fundamentals?.marketCap).toBeDefined();
expect(data.fundamentals?.priceToEarnings).toBeDefined();
});
await tap.test('should fetch complete stock data without fundamentals', async () => {
const data = await service.getStockData({
ticker: 'GOOGL',
includeFundamentals: false
});
expect(data).toBeDefined();
expect(data.ticker).toEqual('GOOGL');
expect(data.price).toBeDefined();
expect(data.fundamentals).toBeUndefined();
});
await tap.test('should handle fundamentals fetch failure gracefully', async () => {
// Try a ticker that might not have fundamentals
const data = await service.getStockData({
ticker: 'BTC-USD', // Crypto - no SEC filings
includeFundamentals: true
});
expect(data).toBeDefined();
expect(data.price).toBeDefined();
// Fundamentals might be undefined due to error
console.log(`\n⚠ ${data.ticker} - Price available, Fundamentals: ${data.fundamentals ? 'Yes' : 'No'}`);
});
});
tap.test('StockDataService - Batch Complete Stock Data', async () => {
const service = new opendata.StockDataService();
const yahooProvider = new opendata.YahooFinanceProvider();
const secProvider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
service.registerPriceProvider(yahooProvider);
service.registerFundamentalsProvider(secProvider);
await tap.test('should fetch batch complete data with array', async () => {
const data = await service.getBatchStockData(['AAPL', 'MSFT']);
expect(data).toBeInstanceOf(Array);
expect(data.length).toEqual(2);
data.forEach(stock => {
expect(stock.ticker).toBeDefined();
expect(stock.price).toBeDefined();
expect(stock.fundamentals).toBeDefined();
expect(stock.fundamentals?.marketCap).toBeGreaterThan(0);
expect(stock.fundamentals?.priceToEarnings).toBeGreaterThan(0);
});
console.log('\n✨ Batch Complete Data:');
data.forEach(stock => {
console.log(` ${stock.ticker}: Price $${stock.price.price.toFixed(2)}, P/E ${stock.fundamentals!.priceToEarnings!.toFixed(2)}`);
});
});
await tap.test('should fetch batch complete data with request object', async () => {
const data = await service.getBatchStockData({
tickers: ['AAPL', 'GOOGL'],
includeFundamentals: true,
enrichFundamentals: true
});
expect(data).toBeInstanceOf(Array);
expect(data.length).toEqual(2);
data.forEach(stock => {
expect(stock.fundamentals?.marketCap).toBeGreaterThan(0);
});
});
await tap.test('should fetch batch without enrichment', async () => {
const data = await service.getBatchStockData({
tickers: ['AAPL', 'MSFT'],
includeFundamentals: true,
enrichFundamentals: false
});
expect(data).toBeInstanceOf(Array);
// Check that fundamentals exist but enrichment might not be complete
data.forEach(stock => {
if (stock.fundamentals) {
expect(stock.fundamentals.ticker).toBeDefined();
expect(stock.fundamentals.companyName).toBeDefined();
}
});
});
});
tap.test('StockDataService - Health & Statistics', async () => {
const service = new opendata.StockDataService();
const yahooProvider = new opendata.YahooFinanceProvider();
const secProvider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
service.registerPriceProvider(yahooProvider);
service.registerFundamentalsProvider(secProvider);
await tap.test('should check providers health', async () => {
const health = await service.checkProvidersHealth();
expect(health.size).toEqual(2);
expect(health.get('Yahoo Finance (price)')).toBe(true);
expect(health.get('SEC EDGAR (fundamentals)')).toBe(true);
console.log('\n💚 Provider Health:');
health.forEach((isHealthy, name) => {
console.log(` ${name}: ${isHealthy ? '✅ Healthy' : '❌ Unhealthy'}`);
});
});
await tap.test('should track provider statistics', async () => {
// Make some requests to generate stats
await service.getPrice('AAPL');
await service.getFundamentals('AAPL');
const stats = service.getProviderStats();
expect(stats.size).toEqual(2);
const yahooStats = stats.get('Yahoo Finance');
expect(yahooStats).toBeDefined();
expect(yahooStats!.type).toEqual('price');
expect(yahooStats!.successCount).toBeGreaterThan(0);
const secStats = stats.get('SEC EDGAR');
expect(secStats).toBeDefined();
expect(secStats!.type).toEqual('fundamentals');
expect(secStats!.successCount).toBeGreaterThan(0);
console.log('\n📈 Provider Statistics:');
stats.forEach((stat, name) => {
console.log(` ${name} (${stat.type}): Success=${stat.successCount}, Errors=${stat.errorCount}`);
});
});
await tap.test('should clear all caches', async () => {
service.clearCache();
const stats = service.getCacheStats();
expect(stats.priceCache.size).toEqual(0);
expect(stats.fundamentalsCache.size).toEqual(0);
});
});
tap.test('StockDataService - Error Handling', async () => {
await tap.test('should throw error when no price provider available', async () => {
const service = new opendata.StockDataService();
try {
await service.getPrice('AAPL');
throw new Error('Should have thrown error');
} catch (error) {
expect(error.message).toContain('No price providers available');
}
});
await tap.test('should throw error when no fundamentals provider available', async () => {
const service = new opendata.StockDataService();
try {
await service.getFundamentals('AAPL');
throw new Error('Should have thrown error');
} catch (error) {
expect(error.message).toContain('No fundamentals providers available');
}
});
await tap.test('should handle invalid ticker for price', async () => {
const service = new opendata.StockDataService();
const yahooProvider = new opendata.YahooFinanceProvider();
service.registerPriceProvider(yahooProvider);
try {
await service.getPrice('INVALIDTICKER123456');
throw new Error('Should have thrown error');
} catch (error) {
expect(error.message).toContain('Failed to fetch price');
}
});
await tap.test('should handle invalid ticker for fundamentals', async () => {
const service = new opendata.StockDataService();
const secProvider = new opendata.SecEdgarProvider({
userAgent: TEST_USER_AGENT
});
service.registerFundamentalsProvider(secProvider);
try {
await service.getFundamentals('INVALIDTICKER123456');
throw new Error('Should have thrown error');
} catch (error) {
expect(error.message).toContain('CIK not found');
}
});
});
export default tap.start();