2025-07-11 08:38:48 +00:00
|
|
|
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 = [
|
2025-07-11 09:09:13 +00:00
|
|
|
// Indices
|
2025-07-11 08:38:48 +00:00
|
|
|
{ ticker: '^GSPC', name: 'S&P 500' },
|
|
|
|
{ ticker: '^IXIC', name: 'NASDAQ' },
|
|
|
|
{ ticker: '^DJI', name: 'DOW Jones' },
|
2025-07-11 09:09:13 +00:00
|
|
|
// 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
|
2025-07-11 08:38:48 +00:00
|
|
|
{ ticker: 'BTC-USD', name: 'Bitcoin' },
|
|
|
|
{ ticker: 'ETH-USD', name: 'Ethereum' },
|
2025-07-11 09:09:13 +00:00
|
|
|
{ ticker: 'ADA-USD', name: 'Cardano' },
|
|
|
|
// Forex & Commodities
|
2025-07-11 08:38:48 +00:00
|
|
|
{ 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:');
|
2025-07-11 09:09:13 +00:00
|
|
|
console.log('═'.repeat(65));
|
2025-07-11 08:38:48 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-11 09:09:13 +00:00
|
|
|
// Display all results with section headers
|
|
|
|
let lastSection = '';
|
2025-07-11 08:38:48 +00:00
|
|
|
for (const indicator of marketIndicators) {
|
2025-07-11 09:09:13 +00:00
|
|
|
// 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';
|
|
|
|
}
|
|
|
|
|
2025-07-11 08:38:48 +00:00
|
|
|
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,
|
2025-07-11 09:09:13 +00:00
|
|
|
maximumFractionDigits: indicator.name.includes('coin') || indicator.name.includes('EUR') || indicator.name === 'Cardano' ? 4 : 2
|
2025-07-11 08:38:48 +00:00
|
|
|
}).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,
|
2025-07-11 09:09:13 +00:00
|
|
|
maximumFractionDigits: indicator.name.includes('coin') || indicator.name.includes('EUR') || indicator.name === 'Cardano' ? 4 : 2
|
2025-07-11 08:38:48 +00:00
|
|
|
}).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`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-11 09:09:13 +00:00
|
|
|
console.log('═'.repeat(65));
|
2025-07-11 08:38:48 +00:00
|
|
|
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();
|