Files
opendata/test/test.stocks.ts
Juergen Kunz a29a50e825 update
2025-07-11 09:09:13 +00:00

280 lines
10 KiB
TypeScript

import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as opendata from '../ts/index.js';
// Test data
const testTickers = ['AAPL', 'MSFT', 'GOOGL'];
const invalidTicker = 'INVALID_TICKER_XYZ';
let stockService: opendata.StockPriceService;
let yahooProvider: opendata.YahooFinanceProvider;
tap.test('should create StockPriceService instance', async () => {
stockService = new opendata.StockPriceService({
ttl: 30000, // 30 seconds cache
maxEntries: 100
});
expect(stockService).toBeInstanceOf(opendata.StockPriceService);
});
tap.test('should create YahooFinanceProvider instance', async () => {
yahooProvider = new opendata.YahooFinanceProvider({
enabled: true,
timeout: 10000,
retryAttempts: 2,
retryDelay: 500
});
expect(yahooProvider).toBeInstanceOf(opendata.YahooFinanceProvider);
expect(yahooProvider.name).toEqual('Yahoo Finance');
expect(yahooProvider.requiresAuth).toEqual(false);
});
tap.test('should register Yahoo provider with the service', async () => {
stockService.register(yahooProvider);
const providers = stockService.getAllProviders();
expect(providers).toContainEqual(yahooProvider);
expect(stockService.getProvider('Yahoo Finance')).toEqual(yahooProvider);
});
tap.test('should check provider health', async () => {
const health = await stockService.checkProvidersHealth();
expect(health.get('Yahoo Finance')).toEqual(true);
});
tap.test('should fetch single stock price', async () => {
const price = await stockService.getPrice({ ticker: 'AAPL' });
expect(price).toHaveProperty('ticker');
expect(price).toHaveProperty('price');
expect(price).toHaveProperty('currency');
expect(price).toHaveProperty('change');
expect(price).toHaveProperty('changePercent');
expect(price).toHaveProperty('previousClose');
expect(price).toHaveProperty('timestamp');
expect(price).toHaveProperty('provider');
expect(price).toHaveProperty('marketState');
expect(price.ticker).toEqual('AAPL');
expect(price.price).toBeGreaterThan(0);
expect(price.provider).toEqual('Yahoo Finance');
expect(price.timestamp).toBeInstanceOf(Date);
});
tap.test('should fetch multiple stock prices', async () => {
const prices = await stockService.getPrices({
tickers: testTickers
});
expect(prices).toBeArray();
expect(prices.length).toBeGreaterThan(0);
expect(prices.length).toBeLessThanOrEqual(testTickers.length);
for (const price of prices) {
expect(testTickers).toContain(price.ticker);
expect(price.price).toBeGreaterThan(0);
expect(price.provider).toEqual('Yahoo Finance');
}
});
tap.test('should serve cached prices on subsequent requests', async () => {
// First request - should hit the API
const firstRequest = await stockService.getPrice({ ticker: 'AAPL' });
// Second request - should be served from cache
const secondRequest = await stockService.getPrice({ ticker: 'AAPL' });
expect(secondRequest.ticker).toEqual(firstRequest.ticker);
expect(secondRequest.price).toEqual(firstRequest.price);
expect(secondRequest.timestamp).toEqual(firstRequest.timestamp);
});
tap.test('should handle invalid ticker gracefully', async () => {
try {
await stockService.getPrice({ ticker: invalidTicker });
throw new Error('Should have thrown an error for invalid ticker');
} catch (error) {
expect(error.message).toInclude('Failed to fetch price');
expect(error.message).toInclude(invalidTicker);
}
});
tap.test('should support market checking', async () => {
expect(yahooProvider.supportsMarket('US')).toEqual(true);
expect(yahooProvider.supportsMarket('UK')).toEqual(true);
expect(yahooProvider.supportsMarket('DE')).toEqual(true);
expect(yahooProvider.supportsMarket('INVALID')).toEqual(false);
});
tap.test('should validate ticker format', async () => {
expect(yahooProvider.supportsTicker('AAPL')).toEqual(true);
expect(yahooProvider.supportsTicker('MSFT')).toEqual(true);
expect(yahooProvider.supportsTicker('BRK.B')).toEqual(true);
expect(yahooProvider.supportsTicker('123456789012')).toEqual(false);
expect(yahooProvider.supportsTicker('invalid@ticker')).toEqual(false);
});
tap.test('should get provider statistics', async () => {
const stats = stockService.getProviderStats();
const yahooStats = stats.get('Yahoo Finance');
expect(yahooStats).not.toEqual(undefined);
expect(yahooStats.successCount).toBeGreaterThan(0);
expect(yahooStats.errorCount).toBeGreaterThanOrEqual(0);
});
tap.test('should clear cache', async () => {
// Ensure we have something in cache
await stockService.getPrice({ ticker: 'AAPL' });
// Clear cache
stockService.clearCache();
// Next request should hit the API again (we can't directly test this,
// but we can verify the method doesn't throw)
const price = await stockService.getPrice({ ticker: 'AAPL' });
expect(price).not.toEqual(undefined);
});
tap.test('should handle provider unavailability', async () => {
// Clear cache first to ensure we don't get cached results
stockService.clearCache();
// Unregister all providers
stockService.unregister('Yahoo Finance');
try {
// Use a different ticker to avoid any caching
await stockService.getPrice({ ticker: 'TSLA' });
throw new Error('Should have thrown an error with no providers');
} catch (error: any) {
expect(error.message).toEqual('No stock price providers available');
}
});
tap.test('should fetch major market indicators', async () => {
// Re-register provider if needed
if (!stockService.getProvider('Yahoo Finance')) {
stockService.register(yahooProvider);
}
const marketIndicators = [
// Indices
{ ticker: '^GSPC', name: 'S&P 500' },
{ ticker: '^IXIC', name: 'NASDAQ' },
{ ticker: '^DJI', name: 'DOW Jones' },
// Tech Stocks
{ ticker: 'AAPL', name: 'Apple' },
{ ticker: 'AMZN', name: 'Amazon' },
{ ticker: 'GOOGL', name: 'Google' },
{ ticker: 'META', name: 'Meta' },
{ ticker: 'MSFT', name: 'Microsoft' },
{ ticker: 'PLTR', name: 'Palantir' },
// Crypto
{ ticker: 'BTC-USD', name: 'Bitcoin' },
{ ticker: 'ETH-USD', name: 'Ethereum' },
{ ticker: 'ADA-USD', name: 'Cardano' },
// Forex & Commodities
{ ticker: 'EURUSD=X', name: 'EUR/USD' },
{ ticker: 'GC=F', name: 'Gold Futures' },
{ ticker: 'CL=F', name: 'Crude Oil Futures' }
];
console.log('\n📊 Current Market Values:');
console.log('═'.repeat(65));
// Fetch all prices in batch for better performance
try {
const prices = await stockService.getPrices({
tickers: marketIndicators.map(i => i.ticker)
});
// Create a map for easy lookup
const priceMap = new Map(prices.map(p => [p.ticker, p]));
// Check which tickers are missing and fetch them individually
const missingTickers: typeof marketIndicators = [];
for (const indicator of marketIndicators) {
if (!priceMap.has(indicator.ticker)) {
missingTickers.push(indicator);
}
}
// Fetch missing tickers individually
if (missingTickers.length > 0) {
for (const indicator of missingTickers) {
try {
const price = await stockService.getPrice({ ticker: indicator.ticker });
priceMap.set(indicator.ticker, price);
} catch (error) {
// Ignore individual errors
}
}
}
// Display all results with section headers
let lastSection = '';
for (const indicator of marketIndicators) {
// Add section headers
if (indicator.ticker.startsWith('^') && lastSection !== 'indices') {
console.log('\n📈 Market Indices');
console.log('─'.repeat(65));
lastSection = 'indices';
} else if (['AAPL', 'AMZN', 'GOOGL', 'META', 'MSFT', 'PLTR'].includes(indicator.ticker) && lastSection !== 'stocks') {
console.log('\n💻 Tech Stocks');
console.log('─'.repeat(65));
lastSection = 'stocks';
} else if (indicator.ticker.includes('-USD') && lastSection !== 'crypto') {
console.log('\n🪙 Cryptocurrencies');
console.log('─'.repeat(65));
lastSection = 'crypto';
} else if ((indicator.ticker.includes('=') || indicator.ticker.includes('=F')) && lastSection !== 'forex') {
console.log('\n💱 Forex & Commodities');
console.log('─'.repeat(65));
lastSection = 'forex';
}
const price = priceMap.get(indicator.ticker);
if (price) {
const changeSymbol = price.change >= 0 ? '↑' : '↓';
const changeColor = price.change >= 0 ? '\x1b[32m' : '\x1b[31m'; // Green or Red
const resetColor = '\x1b[0m';
console.log(
`${indicator.name.padEnd(20)} ${price.price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: indicator.name.includes('coin') || indicator.name.includes('EUR') || indicator.name === 'Cardano' ? 4 : 2
}).padStart(12)} ${changeColor}${changeSymbol} ${price.change >= 0 ? '+' : ''}${price.change.toFixed(2)} (${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%)${resetColor}`
);
} else {
console.log(`${indicator.name.padEnd(20)} Data not available`);
}
}
} catch (error) {
console.log('Error fetching market data:', error);
// Fallback to individual fetches
for (const indicator of marketIndicators) {
try {
const price = await stockService.getPrice({ ticker: indicator.ticker });
const changeSymbol = price.change >= 0 ? '↑' : '↓';
const changeColor = price.change >= 0 ? '\x1b[32m' : '\x1b[31m';
const resetColor = '\x1b[0m';
console.log(
`${indicator.name.padEnd(20)} ${price.price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: indicator.name.includes('coin') || indicator.name.includes('EUR') || indicator.name === 'Cardano' ? 4 : 2
}).padStart(12)} ${changeColor}${changeSymbol} ${price.change >= 0 ? '+' : ''}${price.change.toFixed(2)} (${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%)${resetColor}`
);
} catch (error) {
console.log(`${indicator.name.padEnd(20)} Error fetching data`);
}
}
}
console.log('═'.repeat(65));
console.log(`Last updated: ${new Date().toLocaleString()}\n`);
// Test passes if we successfully fetch at least some indicators
expect(true).toEqual(true);
});
export default tap.start();