feat(stocks/CoinGeckoProvider): Add CoinGecko provider for cryptocurrency prices, export and tests, and update documentation
This commit is contained in:
261
test/test.coingecko.node+bun+deno.ts
Normal file
261
test/test.coingecko.node+bun+deno.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as opendata from '../ts/index.js';
|
||||
|
||||
// Test data
|
||||
const testCryptos = ['BTC', 'ETH', 'USDT'];
|
||||
const testCryptoIds = ['bitcoin', 'ethereum', 'tether'];
|
||||
const invalidCrypto = 'INVALID_CRYPTO_XYZ_12345';
|
||||
|
||||
let stockService: opendata.StockPriceService;
|
||||
let coingeckoProvider: opendata.CoinGeckoProvider;
|
||||
|
||||
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 CoinGeckoProvider instance without API key', async () => {
|
||||
coingeckoProvider = new opendata.CoinGeckoProvider();
|
||||
expect(coingeckoProvider).toBeInstanceOf(opendata.CoinGeckoProvider);
|
||||
expect(coingeckoProvider.name).toEqual('CoinGecko');
|
||||
expect(coingeckoProvider.requiresAuth).toEqual(false);
|
||||
expect(coingeckoProvider.priority).toEqual(90);
|
||||
});
|
||||
|
||||
tap.test('should register CoinGecko provider with the service', async () => {
|
||||
stockService.register(coingeckoProvider);
|
||||
const providers = stockService.getAllProviders();
|
||||
expect(providers).toContainEqual(coingeckoProvider);
|
||||
expect(stockService.getProvider('CoinGecko')).toEqual(coingeckoProvider);
|
||||
});
|
||||
|
||||
tap.test('should check CoinGecko provider health', async () => {
|
||||
const health = await stockService.checkProvidersHealth();
|
||||
expect(health.get('CoinGecko')).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('should fetch single crypto price using ticker symbol (BTC)', async () => {
|
||||
const price = await stockService.getPrice({ ticker: 'BTC' });
|
||||
|
||||
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('BTC');
|
||||
expect(price.price).toBeGreaterThan(0);
|
||||
expect(price.currency).toEqual('USD');
|
||||
expect(price.provider).toEqual('CoinGecko');
|
||||
expect(price.marketState).toEqual('REGULAR'); // Crypto is 24/7
|
||||
expect(price.timestamp).toBeInstanceOf(Date);
|
||||
expect(price.dataType).toEqual('live');
|
||||
|
||||
console.log(`\n📊 BTC Price: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||
console.log(` Change: ${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`);
|
||||
});
|
||||
|
||||
tap.test('should fetch single crypto price using CoinGecko ID (bitcoin)', async () => {
|
||||
// Clear cache to ensure fresh fetch
|
||||
stockService.clearCache();
|
||||
|
||||
const price = await stockService.getPrice({ ticker: 'bitcoin' });
|
||||
|
||||
expect(price.ticker).toEqual('BITCOIN');
|
||||
expect(price.price).toBeGreaterThan(0);
|
||||
expect(price.provider).toEqual('CoinGecko');
|
||||
expect(price.companyName).toInclude('Bitcoin');
|
||||
});
|
||||
|
||||
tap.test('should fetch multiple crypto prices (batch)', async () => {
|
||||
stockService.clearCache();
|
||||
|
||||
const prices = await stockService.getPrices({
|
||||
tickers: testCryptos
|
||||
});
|
||||
|
||||
expect(prices).toBeArray();
|
||||
expect(prices.length).toEqual(testCryptos.length);
|
||||
|
||||
for (const price of prices) {
|
||||
expect(testCryptos).toContain(price.ticker);
|
||||
expect(price.price).toBeGreaterThan(0);
|
||||
expect(price.provider).toEqual('CoinGecko');
|
||||
expect(price.marketState).toEqual('REGULAR');
|
||||
|
||||
console.log(`\n💰 ${price.ticker}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 8 })}`);
|
||||
console.log(` Change 24h: ${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`);
|
||||
if (price.volume) {
|
||||
console.log(` Volume 24h: $${price.volume.toLocaleString('en-US')}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should fetch historical crypto prices', async () => {
|
||||
// Add delay to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago
|
||||
|
||||
const prices = await stockService.getData({
|
||||
type: 'historical',
|
||||
ticker: 'BTC',
|
||||
from: from,
|
||||
to: to
|
||||
});
|
||||
|
||||
expect(prices).toBeArray();
|
||||
expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0);
|
||||
|
||||
const pricesArray = prices as opendata.IStockPrice[];
|
||||
console.log(`\n📈 Historical BTC Prices (${pricesArray.length} days):`);
|
||||
|
||||
// Show first few and last few
|
||||
const toShow = Math.min(3, pricesArray.length);
|
||||
for (let i = 0; i < toShow; i++) {
|
||||
const price = pricesArray[i];
|
||||
const date = price.timestamp.toISOString().split('T')[0];
|
||||
console.log(` ${date}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||
}
|
||||
|
||||
if (pricesArray.length > toShow * 2) {
|
||||
console.log(' ...');
|
||||
}
|
||||
|
||||
for (let i = Math.max(toShow, pricesArray.length - toShow); i < pricesArray.length; i++) {
|
||||
const price = pricesArray[i];
|
||||
const date = price.timestamp.toISOString().split('T')[0];
|
||||
console.log(` ${date}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||
}
|
||||
|
||||
// Validate first entry
|
||||
const firstPrice = pricesArray[0];
|
||||
expect(firstPrice.ticker).toEqual('BTC');
|
||||
expect(firstPrice.dataType).toEqual('eod');
|
||||
expect(firstPrice.provider).toEqual('CoinGecko');
|
||||
});
|
||||
|
||||
tap.test('should fetch intraday crypto prices (hourly)', async () => {
|
||||
// Add delay to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const prices = await stockService.getData({
|
||||
type: 'intraday',
|
||||
ticker: 'ETH',
|
||||
interval: '1hour',
|
||||
limit: 12 // Last 12 hours
|
||||
});
|
||||
|
||||
expect(prices).toBeArray();
|
||||
expect((prices as opendata.IStockPrice[]).length).toBeGreaterThan(0);
|
||||
|
||||
const pricesArray = prices as opendata.IStockPrice[];
|
||||
console.log(`\n⏰ Intraday ETH Prices (hourly, last ${pricesArray.length} hours):`);
|
||||
|
||||
// Show first few entries
|
||||
const toShow = Math.min(5, pricesArray.length);
|
||||
for (let i = 0; i < toShow; i++) {
|
||||
const price = pricesArray[i];
|
||||
const time = price.timestamp.toISOString().replace('T', ' ').substring(0, 16);
|
||||
console.log(` ${time}: $${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
|
||||
}
|
||||
|
||||
// Validate first entry
|
||||
const firstPrice = pricesArray[0];
|
||||
expect(firstPrice.ticker).toEqual('ETH');
|
||||
expect(firstPrice.dataType).toEqual('intraday');
|
||||
expect(firstPrice.provider).toEqual('CoinGecko');
|
||||
});
|
||||
|
||||
tap.test('should serve cached prices on subsequent requests', async () => {
|
||||
// First request - should hit the API
|
||||
const firstRequest = await stockService.getPrice({ ticker: 'BTC' });
|
||||
|
||||
// Second request - should be served from cache
|
||||
const secondRequest = await stockService.getPrice({ ticker: 'BTC' });
|
||||
|
||||
expect(secondRequest.ticker).toEqual(firstRequest.ticker);
|
||||
expect(secondRequest.price).toEqual(firstRequest.price);
|
||||
expect(secondRequest.timestamp).toEqual(firstRequest.timestamp);
|
||||
expect(secondRequest.fetchedAt).toEqual(firstRequest.fetchedAt);
|
||||
});
|
||||
|
||||
tap.test('should handle invalid crypto ticker gracefully', async () => {
|
||||
try {
|
||||
await stockService.getPrice({ ticker: invalidCrypto });
|
||||
throw new Error('Should have thrown an error for invalid ticker');
|
||||
} catch (error) {
|
||||
expect(error.message).toInclude('Failed to fetch');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should support market checking', async () => {
|
||||
expect(coingeckoProvider.supportsMarket('CRYPTO')).toEqual(true);
|
||||
expect(coingeckoProvider.supportsMarket('BTC')).toEqual(true);
|
||||
expect(coingeckoProvider.supportsMarket('ETH')).toEqual(true);
|
||||
expect(coingeckoProvider.supportsMarket('NASDAQ')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('should support ticker validation', async () => {
|
||||
expect(coingeckoProvider.supportsTicker('BTC')).toEqual(true);
|
||||
expect(coingeckoProvider.supportsTicker('bitcoin')).toEqual(true);
|
||||
expect(coingeckoProvider.supportsTicker('wrapped-bitcoin')).toEqual(true);
|
||||
expect(coingeckoProvider.supportsTicker('BTC!')).toEqual(false);
|
||||
expect(coingeckoProvider.supportsTicker('BTC@USD')).toEqual(false);
|
||||
});
|
||||
|
||||
tap.test('should display provider statistics', async () => {
|
||||
const stats = stockService.getProviderStats();
|
||||
const coingeckoStats = stats.get('CoinGecko');
|
||||
|
||||
expect(coingeckoStats).toBeTruthy();
|
||||
expect(coingeckoStats.successCount).toBeGreaterThan(0);
|
||||
|
||||
console.log('\n📊 CoinGecko Provider Statistics:');
|
||||
console.log(` Success Count: ${coingeckoStats.successCount}`);
|
||||
console.log(` Error Count: ${coingeckoStats.errorCount}`);
|
||||
if (coingeckoStats.lastError) {
|
||||
console.log(` Last Error: ${coingeckoStats.lastError}`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should display crypto price dashboard', async () => {
|
||||
// Add delay to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
stockService.clearCache();
|
||||
|
||||
const cryptos = ['BTC', 'ETH', 'BNB', 'SOL', 'ADA'];
|
||||
const prices = await stockService.getPrices({ tickers: cryptos });
|
||||
|
||||
console.log('\n╔═══════════════════════════════════════════════════════════╗');
|
||||
console.log('║ 🌐 CRYPTOCURRENCY PRICE DASHBOARD ║');
|
||||
console.log('╠═══════════════════════════════════════════════════════════╣');
|
||||
|
||||
for (const price of prices) {
|
||||
const priceStr = `$${price.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 8 })}`;
|
||||
const changeStr = `${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%`;
|
||||
const changeIcon = price.changePercent >= 0 ? '📈' : '📉';
|
||||
|
||||
console.log(`║ ${price.ticker.padEnd(6)} ${changeIcon} ${priceStr.padStart(20)} │ ${changeStr.padStart(10)} ║`);
|
||||
}
|
||||
|
||||
console.log('╚═══════════════════════════════════════════════════════════╝');
|
||||
console.log(`Provider: ${prices[0].provider} | Market State: ${prices[0].marketState} (24/7)`);
|
||||
console.log(`Fetched at: ${prices[0].fetchedAt.toISOString()}`);
|
||||
});
|
||||
|
||||
tap.test('should clear cache', async () => {
|
||||
stockService.clearCache();
|
||||
// Cache is cleared, no assertions needed
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user