From ec3e4dde751f17efbaa3762e9447da8126231499 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 31 Oct 2025 15:05:48 +0000 Subject: [PATCH] BREAKING CHANGE(stocks): Unify stock provider API to discriminated IStockDataRequest and add company name/fullname enrichment --- changelog.md | 11 ++ readme.md | 19 ++- test/test.marketstack.node.ts | 137 ++++++++++++++-- ts/00_commitinfo_data.ts | 2 +- ts/stocks/classes.stockservice.ts | 168 ++++---------------- ts/stocks/interfaces/provider.ts | 11 +- ts/stocks/interfaces/stockprice.ts | 23 +-- ts/stocks/providers/provider.marketstack.ts | 69 ++++---- ts/stocks/providers/provider.yahoo.ts | 37 ++++- 9 files changed, 266 insertions(+), 211 deletions(-) diff --git a/changelog.md b/changelog.md index 70bd77b..f539472 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-10-31 - 3.0.0 - BREAKING CHANGE(stocks) +Unify stock provider API to discriminated IStockDataRequest and add company name/fullname enrichment + +- Replace legacy provider methods (fetchPrice/fetchPrices) with a single fetchData(request: IStockDataRequest) on IStockProvider — providers must be migrated to the new signature. +- Migrate StockPriceService to the unified getData(request: IStockDataRequest) API. Convenience helpers getPrice/getPrices now wrap getData. +- Add companyName and companyFullName fields to IStockPrice and populate them in provider mappings (Marketstack mapping updated; Yahoo provider updated to support the unified API). +- MarketstackProvider: added buildCompanyFullName helper and improved mapping to include company identification fields and full name formatting. +- YahooFinanceProvider: updated to implement fetchData and to route current/batch requests through the new unified request types; historical/intraday throw explicit errors. +- Updated tests to exercise the new unified API, company-name enrichment, caching behavior, and provider direct methods. +- Note: This is a breaking change for external providers and integrations that implemented the old fetchPrice/fetchPrices API. Bump major version. + ## 2025-10-31 - 2.1.0 - feat(stocks) Add unified stock data API (getData) with historical/OHLCV support, smart caching and provider enhancements diff --git a/readme.md b/readme.md index 7d9b382..69164a6 100644 --- a/readme.md +++ b/readme.md @@ -93,6 +93,8 @@ stockService.register(new MarketstackProvider('YOUR_API_KEY'), { const apple = await stockService.getData({ type: 'current', ticker: 'AAPL' }); console.log(`Apple: $${apple.price} (${apple.changePercent.toFixed(2)}%)`); console.log(`OHLCV: O=${apple.open} H=${apple.high} L=${apple.low} V=${apple.volume}`); +console.log(`Company: ${apple.companyName}`); // "Apple Inc" +console.log(`Full: ${apple.companyFullName}`); // "Apple Inc (NASDAQ:AAPL)" // Get historical data (1 year of daily prices) const history = await stockService.getData({ @@ -117,11 +119,25 @@ const vodNYSE = await stockService.getData({ exchange: 'XNYS' // New York Stock Exchange }); -// Batch current prices +// Batch current prices with company names const prices = await stockService.getData({ type: 'batch', tickers: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA'] }); + +// Display with company names (automatically included - zero extra API calls!) +for (const stock of prices) { + console.log(`${stock.companyName}: $${stock.price}`); + // Output: + // Apple Inc: $271.40 + // Microsoft Corporation: $525.76 + // Alphabet Inc - Class A: $281.48 + // Amazon.com Inc: $222.86 + // Tesla Inc: $440.10 +} + +// Use companyFullName for richer context +console.log(prices[0].companyFullName); // "Apple Inc (NASDAQ:AAPL)" ``` ### šŸ¢ German Business Data @@ -164,6 +180,7 @@ await openData.buildInitialDb(); ### šŸŽÆ Stock Market Module (v2.1 Enhanced) +- **Company Names** - Automatic company name extraction with zero extra API calls (e.g., "Apple Inc (NASDAQ:AAPL)") - **Historical Data** - Up to 15 years of daily EOD prices with automatic pagination - **Exchange Filtering** - Query specific exchanges via MIC codes (XNAS, XLON, XNYS, etc.) - **OHLCV Data** - Open, High, Low, Close, Volume for comprehensive analysis diff --git a/test/test.marketstack.node.ts b/test/test.marketstack.node.ts index 91a0e94..bbf9ef6 100644 --- a/test/test.marketstack.node.ts +++ b/test/test.marketstack.node.ts @@ -151,7 +151,7 @@ tap.test('should handle invalid ticker gracefully', async () => { 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('Failed to fetch'); console.log('āœ“ Invalid ticker handled correctly'); } }); @@ -215,19 +215,20 @@ tap.test('should test direct provider methods', async () => { expect(available).toEqual(true); console.log(' āœ“ isAvailable() returned true'); - // Test fetchPrice directly - const price = await marketstackProvider.fetchPrice({ ticker: 'MSFT' }); + // Test fetchData for single ticker + const price = await marketstackProvider.fetchData({ type: 'current', ticker: 'MSFT' }) as opendata.IStockPrice; expect(price.ticker).toEqual('MSFT'); expect(price.provider).toEqual('Marketstack'); expect(price.price).toBeGreaterThan(0); - console.log(` āœ“ fetchPrice() for MSFT: $${price.price}`); + console.log(` āœ“ fetchData (current) for MSFT: $${price.price}`); - // Test fetchPrices directly - const prices = await marketstackProvider.fetchPrices({ + // Test fetchData for batch + const prices = await marketstackProvider.fetchData({ + type: 'batch', tickers: ['AAPL', 'GOOGL'] - }); + }) as opendata.IStockPrice[]; expect(prices.length).toBeGreaterThan(0); - console.log(` āœ“ fetchPrices() returned ${prices.length} prices`); + console.log(` āœ“ fetchData (batch) returned ${prices.length} prices`); for (const p of prices) { console.log(` ${p.ticker}: $${p.price}`); @@ -252,9 +253,10 @@ tap.test('should fetch sample EOD data', async () => { ]; try { - const prices = await marketstackProvider.fetchPrices({ + const prices = await marketstackProvider.fetchData({ + type: 'batch', tickers: sampleTickers.map(t => t.ticker) - }); + }) as opendata.IStockPrice[]; const priceMap = new Map(prices.map(p => [p.ticker, p])); @@ -452,4 +454,119 @@ tap.test('should verify smart caching with historical data', async () => { console.log(`āœ“ Speed improvement: ${Math.round((duration1 / duration2) * 10) / 10}x faster`); }); +// Company Name Feature Tests + +tap.test('should include company name in single price request', async () => { + if (!marketstackProvider) { + console.log('āš ļø Skipping - Marketstack provider not initialized'); + return; + } + + console.log('\nšŸ¢ Testing Company Name Feature: Single Request'); + + const price = await stockService.getPrice({ ticker: 'AAPL' }); + + expect(price.companyName).not.toEqual(undefined); + expect(typeof price.companyName).toEqual('string'); + expect(price.companyName).toInclude('Apple'); + + console.log(`āœ“ Company name retrieved: "${price.companyName}"`); + console.log(` Ticker: ${price.ticker}`); + console.log(` Price: $${price.price}`); + console.log(` Company: ${price.companyName}`); +}); + +tap.test('should include company names in batch price request', async () => { + if (!marketstackProvider) { + console.log('āš ļø Skipping - Marketstack provider not initialized'); + return; + } + + console.log('\nšŸ¢ Testing Company Name Feature: Batch Request'); + + const prices = await stockService.getPrices({ + tickers: ['AAPL', 'MSFT', 'GOOGL'] + }); + + expect(prices).toBeArray(); + expect(prices.length).toBeGreaterThan(0); + + console.log(`āœ“ Fetched ${prices.length} prices with company names:`); + + for (const price of prices) { + expect(price.companyName).not.toEqual(undefined); + expect(typeof price.companyName).toEqual('string'); + console.log(` ${price.ticker.padEnd(6)} - ${price.companyName}`); + } +}); + +tap.test('should include company name in historical data', async () => { + if (!marketstackProvider) { + console.log('āš ļø Skipping - Marketstack provider not initialized'); + return; + } + + console.log('\nšŸ¢ Testing Company Name Feature: Historical Data'); + + const prices = await stockService.getData({ + type: 'historical', + ticker: 'TSLA', + from: new Date('2025-10-01'), + to: new Date('2025-10-05') + }); + + expect(prices).toBeArray(); + const historicalPrices = prices as opendata.IStockPrice[]; + expect(historicalPrices.length).toBeGreaterThan(0); + + // All historical records should have the same company name + for (const price of historicalPrices) { + expect(price.companyName).not.toEqual(undefined); + expect(typeof price.companyName).toEqual('string'); + } + + const firstPrice = historicalPrices[0]; + console.log(`āœ“ Historical records include company name: "${firstPrice.companyName}"`); + console.log(` Ticker: ${firstPrice.ticker}`); + console.log(` Records: ${historicalPrices.length}`); + console.log(` Date range: ${historicalPrices[historicalPrices.length - 1].timestamp.toISOString().split('T')[0]} to ${firstPrice.timestamp.toISOString().split('T')[0]}`); +}); + +tap.test('should verify company name is included with zero extra API calls', async () => { + if (!marketstackProvider) { + console.log('āš ļø Skipping - Marketstack provider not initialized'); + return; + } + + console.log('\n⚔ Testing Company Name Efficiency: Zero Extra API Calls'); + + // Clear cache to ensure we're making fresh API calls + stockService.clearCache(); + + // Single request timing + const start1 = Date.now(); + const singlePrice = await stockService.getPrice({ ticker: 'AMZN' }); + const duration1 = Date.now() - start1; + + expect(singlePrice.companyName).not.toEqual(undefined); + + // Batch request timing + stockService.clearCache(); + const start2 = Date.now(); + const batchPrices = await stockService.getPrices({ tickers: ['NVDA', 'AMD', 'INTC'] }); + const duration2 = Date.now() - start2; + + for (const price of batchPrices) { + expect(price.companyName).not.toEqual(undefined); + } + + console.log(`āœ“ Single request (with company name): ${duration1}ms`); + console.log(`āœ“ Batch request (with company names): ${duration2}ms`); + console.log(`āœ“ Company names included in standard EOD response - zero extra calls!`); + console.log(` Single: ${singlePrice.ticker} - "${singlePrice.companyName}"`); + for (const price of batchPrices) { + console.log(` Batch: ${price.ticker} - "${price.companyName}"`); + } +}); + export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 5283428..b72a1f5 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@fin.cx/opendata', - version: '2.1.0', + version: '3.0.0', description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.' } diff --git a/ts/stocks/classes.stockservice.ts b/ts/stocks/classes.stockservice.ts index 633fbd9..5024afb 100644 --- a/ts/stocks/classes.stockservice.ts +++ b/ts/stocks/classes.stockservice.ts @@ -2,8 +2,6 @@ import * as plugins from '../plugins.js'; import type { IStockProvider, IProviderConfig, IProviderRegistry } from './interfaces/provider.js'; import type { IStockPrice, - IStockQuoteRequest, - IStockBatchQuoteRequest, IStockPriceError, IStockDataRequest, IStockCurrentRequest, @@ -13,6 +11,15 @@ import type { TIntervalType } from './interfaces/stockprice.js'; +// Simple request interfaces for convenience methods +interface ISimpleQuoteRequest { + ticker: string; +} + +interface ISimpleBatchRequest { + tickers: string[]; +} + interface IProviderEntry { provider: IStockProvider; config: IProviderConfig; @@ -122,132 +129,26 @@ export class StockPriceService implements IProviderRegistry { .map(entry => entry.provider); } - public async getPrice(request: IStockQuoteRequest): Promise { - const cacheKey = this.getCacheKey(request); - const cached = this.getFromCache(cacheKey) as IStockPrice | null; - - if (cached) { - console.log(`Cache hit for ${request.ticker}`); - return cached; - } - - const providers = this.getEnabledProviders(); - if (providers.length === 0) { - throw new Error('No stock price providers available'); - } - - let lastError: Error | undefined; - - for (const provider of providers) { - const entry = this.providers.get(provider.name)!; - - try { - const price = await this.fetchWithRetry( - () => provider.fetchPrice(request), - entry.config - ); - - entry.successCount++; - - // Use smart TTL based on data type - const ttl = this.getCacheTTL(price.dataType); - this.addToCache(cacheKey, price, ttl); - - console.log(`Successfully fetched ${request.ticker} from ${provider.name}`); - return price; - } catch (error) { - entry.errorCount++; - entry.lastError = error as Error; - entry.lastErrorTime = new Date(); - lastError = error as Error; - - console.warn( - `Provider ${provider.name} failed for ${request.ticker}: ${error.message}` - ); - } - } - - throw new Error( - `Failed to fetch price for ${request.ticker} from all providers. Last error: ${lastError?.message}` - ); + /** + * Convenience method: Get current price for a single ticker + */ + public async getPrice(request: ISimpleQuoteRequest): Promise { + const result = await this.getData({ + type: 'current', + ticker: request.ticker + }); + return result as IStockPrice; } - public async getPrices(request: IStockBatchQuoteRequest): Promise { - const cachedPrices: IStockPrice[] = []; - const tickersToFetch: string[] = []; - - // Check cache for each ticker - for (const ticker of request.tickers) { - const cacheKey = this.getCacheKey({ ticker, includeExtendedHours: request.includeExtendedHours }); - const cached = this.getFromCache(cacheKey) as IStockPrice | null; - - if (cached) { - cachedPrices.push(cached); - } else { - tickersToFetch.push(ticker); - } - } - - if (tickersToFetch.length === 0) { - console.log(`All ${request.tickers.length} tickers served from cache`); - return cachedPrices; - } - - const providers = this.getEnabledProviders(); - if (providers.length === 0) { - throw new Error('No stock price providers available'); - } - - let lastError: Error | undefined; - let fetchedPrices: IStockPrice[] = []; - - for (const provider of providers) { - const entry = this.providers.get(provider.name)!; - - try { - fetchedPrices = await this.fetchWithRetry( - () => provider.fetchPrices({ - tickers: tickersToFetch, - includeExtendedHours: request.includeExtendedHours - }), - entry.config - ); - - entry.successCount++; - - // Cache the fetched prices with smart TTL - for (const price of fetchedPrices) { - const cacheKey = this.getCacheKey({ - ticker: price.ticker, - includeExtendedHours: request.includeExtendedHours - }); - const ttl = this.getCacheTTL(price.dataType); - this.addToCache(cacheKey, price, ttl); - } - - console.log( - `Successfully fetched ${fetchedPrices.length} prices from ${provider.name}` - ); - break; - } catch (error) { - entry.errorCount++; - entry.lastError = error as Error; - entry.lastErrorTime = new Date(); - lastError = error as Error; - - console.warn( - `Provider ${provider.name} failed for batch request: ${error.message}` - ); - } - } - - if (fetchedPrices.length === 0 && lastError) { - throw new Error( - `Failed to fetch prices from all providers. Last error: ${lastError.message}` - ); - } - - return [...cachedPrices, ...fetchedPrices]; + /** + * Convenience method: Get current prices for multiple tickers + */ + public async getPrices(request: ISimpleBatchRequest): Promise { + const result = await this.getData({ + type: 'batch', + tickers: request.tickers + }); + return result as IStockPrice[]; } /** @@ -272,15 +173,9 @@ export class StockPriceService implements IProviderRegistry { for (const provider of providers) { const entry = this.providers.get(provider.name)!; - // Check if provider supports the new fetchData method - if (typeof (provider as any).fetchData !== 'function') { - console.warn(`Provider ${provider.name} does not support new API, skipping`); - continue; - } - try { const result = await this.fetchWithRetry( - () => (provider as any).fetchData(request), + () => provider.fetchData(request), entry.config ) as IStockPrice | IStockPrice[]; @@ -420,13 +315,6 @@ export class StockPriceService implements IProviderRegistry { throw lastError || new Error('Unknown error during fetch'); } - /** - * Legacy cache key generation - */ - private getCacheKey(request: IStockQuoteRequest): string { - return `${request.ticker}:${request.includeExtendedHours || false}`; - } - /** * New cache key generation for discriminated union requests */ diff --git a/ts/stocks/interfaces/provider.ts b/ts/stocks/interfaces/provider.ts index 766d714..dbd1375 100644 --- a/ts/stocks/interfaces/provider.ts +++ b/ts/stocks/interfaces/provider.ts @@ -1,16 +1,15 @@ -import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from './stockprice.js'; +import type { IStockPrice, IStockDataRequest } from './stockprice.js'; export interface IStockProvider { name: string; priority: number; - - fetchPrice(request: IStockQuoteRequest): Promise; - fetchPrices(request: IStockBatchQuoteRequest): Promise; + + fetchData(request: IStockDataRequest): Promise; isAvailable(): Promise; - + supportsMarket?(market: string): boolean; supportsTicker?(ticker: string): boolean; - + readonly requiresAuth: boolean; readonly rateLimit?: { requestsPerMinute: number; diff --git a/ts/stocks/interfaces/stockprice.ts b/ts/stocks/interfaces/stockprice.ts index 0b74895..e8fb495 100644 --- a/ts/stocks/interfaces/stockprice.ts +++ b/ts/stocks/interfaces/stockprice.ts @@ -1,5 +1,3 @@ -import * as plugins from '../../plugins.js'; - // Enhanced stock price interface with additional OHLCV data export interface IStockPrice { ticker: string; @@ -22,11 +20,11 @@ export interface IStockPrice { adjusted?: boolean; // If price is split/dividend adjusted dataType: 'eod' | 'intraday' | 'live'; // What kind of data this is fetchedAt: Date; // When we fetched (vs data timestamp) + + // Company identification + companyName?: string; // Company name (e.g., "Apple Inc.") + companyFullName?: string; // Full company name with exchange (e.g., "Apple Inc. (NASDAQ:AAPL)") } -type CheckStockPrice = plugins.tsclass.typeFest.IsEqual< - IStockPrice, - plugins.tsclass.finance.IStockPrice ->; export interface IStockPriceError { ticker: string; @@ -94,16 +92,3 @@ export type IStockDataRequest = | IStockHistoricalRequest | IStockIntradayRequest | IStockBatchCurrentRequest; - -// Legacy interfaces (for backward compatibility during migration) -/** @deprecated Use IStockDataRequest with type: 'current' instead */ -export interface IStockQuoteRequest { - ticker: string; - includeExtendedHours?: boolean; -} - -/** @deprecated Use IStockDataRequest with type: 'batch' instead */ -export interface IStockBatchQuoteRequest { - tickers: string[]; - includeExtendedHours?: boolean; -} diff --git a/ts/stocks/providers/provider.marketstack.ts b/ts/stocks/providers/provider.marketstack.ts index 11751f1..7d35b93 100644 --- a/ts/stocks/providers/provider.marketstack.ts +++ b/ts/stocks/providers/provider.marketstack.ts @@ -2,15 +2,11 @@ import * as plugins from '../../plugins.js'; import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js'; import type { IStockPrice, - IStockQuoteRequest, - IStockBatchQuoteRequest, IStockDataRequest, IStockCurrentRequest, IStockHistoricalRequest, IStockIntradayRequest, - IStockBatchCurrentRequest, - IPaginatedResponse, - TSortOrder + IStockBatchCurrentRequest } from '../interfaces/stockprice.js'; /** @@ -239,30 +235,6 @@ export class MarketstackProvider implements IStockProvider { } } - /** - * Legacy: Fetch latest EOD price for a single ticker - * @deprecated Use fetchData with IStockDataRequest instead - */ - public async fetchPrice(request: IStockQuoteRequest): Promise { - // Map legacy request to new format - return this.fetchCurrentPrice({ - type: 'current', - ticker: request.ticker - }); - } - - /** - * Legacy: Fetch latest EOD prices for multiple tickers - * @deprecated Use fetchData with IStockDataRequest instead - */ - public async fetchPrices(request: IStockBatchQuoteRequest): Promise { - // Map legacy request to new format - return this.fetchBatchCurrentPrices({ - type: 'batch', - tickers: request.tickers - }); - } - /** * Check if the Marketstack API is available and accessible */ @@ -349,12 +321,49 @@ export class MarketstackProvider implements IStockProvider { low: data.low, adjusted: data.adj_close !== undefined, // If adj_close exists, price is adjusted dataType: dataType, - fetchedAt: fetchedAt + fetchedAt: fetchedAt, + + // Company identification + companyName: data.company_name || data.name || undefined, + companyFullName: this.buildCompanyFullName(data) }; return stockPrice; } + /** + * Build full company name with exchange and ticker information + * Example: "Apple Inc (NASDAQ:AAPL)" + */ + private buildCompanyFullName(data: any): string | undefined { + // Check if API already provides full name + if (data.full_name || data.long_name) { + return data.full_name || data.long_name; + } + + // Build from available data + const companyName = data.company_name || data.name; + const exchangeCode = data.exchange_code; // e.g., "NASDAQ" + const symbol = data.symbol; // e.g., "AAPL" + + if (!companyName) { + return undefined; + } + + // If we have exchange and symbol, build full name: "Apple Inc (NASDAQ:AAPL)" + if (exchangeCode && symbol) { + return `${companyName} (${exchangeCode}:${symbol})`; + } + + // If we only have symbol: "Apple Inc (AAPL)" + if (symbol) { + return `${companyName} (${symbol})`; + } + + // Otherwise just return company name + return companyName; + } + /** * Format date to YYYY-MM-DD for API requests */ diff --git a/ts/stocks/providers/provider.yahoo.ts b/ts/stocks/providers/provider.yahoo.ts index 59df9d6..637cb1a 100644 --- a/ts/stocks/providers/provider.yahoo.ts +++ b/ts/stocks/providers/provider.yahoo.ts @@ -1,6 +1,11 @@ import * as plugins from '../../plugins.js'; import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js'; -import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from '../interfaces/stockprice.js'; +import type { + IStockPrice, + IStockDataRequest, + IStockCurrentRequest, + IStockBatchCurrentRequest +} from '../interfaces/stockprice.js'; export class YahooFinanceProvider implements IStockProvider { public name = 'Yahoo Finance'; @@ -17,7 +22,28 @@ export class YahooFinanceProvider implements IStockProvider { constructor(private config?: IProviderConfig) {} - public async fetchPrice(request: IStockQuoteRequest): Promise { + /** + * Unified data fetching method + */ + public async fetchData(request: IStockDataRequest): Promise { + switch (request.type) { + case 'current': + return this.fetchCurrentPrice(request); + case 'batch': + return this.fetchBatchCurrentPrices(request); + case 'historical': + throw new Error('Yahoo Finance provider does not support historical data. Use Marketstack provider instead.'); + case 'intraday': + throw new Error('Yahoo Finance provider does not support intraday data yet. Use Marketstack provider instead.'); + default: + throw new Error(`Unsupported request type: ${(request as any).type}`); + } + } + + /** + * Fetch current price for a single ticker + */ + private async fetchCurrentPrice(request: IStockCurrentRequest): Promise { try { const url = `${this.baseUrl}/v8/finance/chart/${request.ticker}`; const response = await plugins.smartrequest.SmartRequest.create() @@ -64,7 +90,10 @@ export class YahooFinanceProvider implements IStockProvider { } } - public async fetchPrices(request: IStockBatchQuoteRequest): Promise { + /** + * Fetch batch current prices + */ + private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise { try { const symbols = request.tickers.join(','); const url = `${this.baseUrl}/v8/finance/spark?symbols=${symbols}&range=1d&interval=5m`; @@ -123,7 +152,7 @@ export class YahooFinanceProvider implements IStockProvider { public async isAvailable(): Promise { try { // Test with a well-known ticker - await this.fetchPrice({ ticker: 'AAPL' }); + await this.fetchData({ type: 'current', ticker: 'AAPL' }); return true; } catch (error) { console.warn('Yahoo Finance provider is not available:', error);