648 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			648 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import * as plugins from '../plugins.js';
 | 
						||
import type { IStockProvider, IProviderConfig } from './interfaces/provider.js';
 | 
						||
import type { IFundamentalsProvider, IFundamentalsProviderConfig, IStockFundamentals } from './interfaces/fundamentals.js';
 | 
						||
import type { IStockPrice, IStockDataRequest as IPriceRequest } from './interfaces/stockprice.js';
 | 
						||
import type { IStockData, IStockDataServiceConfig, ICompleteStockDataRequest, ICompleteStockDataBatchRequest } from './interfaces/stockdata.js';
 | 
						||
 | 
						||
interface IProviderEntry<T> {
 | 
						||
  provider: T;
 | 
						||
  config: IProviderConfig | IFundamentalsProviderConfig;
 | 
						||
  lastError?: Error;
 | 
						||
  lastErrorTime?: Date;
 | 
						||
  successCount: number;
 | 
						||
  errorCount: number;
 | 
						||
}
 | 
						||
 | 
						||
interface ICacheEntry<T> {
 | 
						||
  data: T;
 | 
						||
  timestamp: Date;
 | 
						||
  ttl: number;
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * Unified service for managing both stock prices and fundamentals
 | 
						||
 * Provides automatic enrichment and convenient combined data access
 | 
						||
 */
 | 
						||
export class StockDataService {
 | 
						||
  private priceProviders = new Map<string, IProviderEntry<IStockProvider>>();
 | 
						||
  private fundamentalsProviders = new Map<string, IProviderEntry<IFundamentalsProvider>>();
 | 
						||
 | 
						||
  private priceCache = new Map<string, ICacheEntry<IStockPrice | IStockPrice[]>>();
 | 
						||
  private fundamentalsCache = new Map<string, ICacheEntry<IStockFundamentals | IStockFundamentals[]>>();
 | 
						||
 | 
						||
  private logger = console;
 | 
						||
 | 
						||
  private config: Required<IStockDataServiceConfig> = {
 | 
						||
    cache: {
 | 
						||
      priceTTL: 24 * 60 * 60 * 1000, // 24 hours
 | 
						||
      fundamentalsTTL: 90 * 24 * 60 * 60 * 1000, // 90 days
 | 
						||
      maxEntries: 10000
 | 
						||
    },
 | 
						||
    timeout: {
 | 
						||
      price: 10000, // 10 seconds
 | 
						||
      fundamentals: 30000 // 30 seconds
 | 
						||
    }
 | 
						||
  };
 | 
						||
 | 
						||
  constructor(config?: IStockDataServiceConfig) {
 | 
						||
    if (config) {
 | 
						||
      this.config = {
 | 
						||
        cache: { ...this.config.cache, ...config.cache },
 | 
						||
        timeout: { ...this.config.timeout, ...config.timeout }
 | 
						||
      };
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  // ========== Provider Management ==========
 | 
						||
 | 
						||
  /**
 | 
						||
   * Register a price provider
 | 
						||
   */
 | 
						||
  public registerPriceProvider(provider: IStockProvider, config?: IProviderConfig): void {
 | 
						||
    const defaultConfig: IProviderConfig = {
 | 
						||
      enabled: true,
 | 
						||
      priority: provider.priority,
 | 
						||
      timeout: this.config.timeout.price,
 | 
						||
      retryAttempts: 2,
 | 
						||
      retryDelay: 1000
 | 
						||
    };
 | 
						||
 | 
						||
    const mergedConfig = { ...defaultConfig, ...config };
 | 
						||
 | 
						||
    this.priceProviders.set(provider.name, {
 | 
						||
      provider,
 | 
						||
      config: mergedConfig,
 | 
						||
      successCount: 0,
 | 
						||
      errorCount: 0
 | 
						||
    });
 | 
						||
 | 
						||
    console.log(`Registered price provider: ${provider.name}`);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Register a fundamentals provider
 | 
						||
   */
 | 
						||
  public registerFundamentalsProvider(
 | 
						||
    provider: IFundamentalsProvider,
 | 
						||
    config?: IFundamentalsProviderConfig
 | 
						||
  ): void {
 | 
						||
    const defaultConfig: IFundamentalsProviderConfig = {
 | 
						||
      enabled: true,
 | 
						||
      priority: provider.priority,
 | 
						||
      timeout: this.config.timeout.fundamentals,
 | 
						||
      retryAttempts: 2,
 | 
						||
      retryDelay: 1000,
 | 
						||
      cacheTTL: this.config.cache.fundamentalsTTL
 | 
						||
    };
 | 
						||
 | 
						||
    const mergedConfig = { ...defaultConfig, ...config };
 | 
						||
 | 
						||
    this.fundamentalsProviders.set(provider.name, {
 | 
						||
      provider,
 | 
						||
      config: mergedConfig,
 | 
						||
      successCount: 0,
 | 
						||
      errorCount: 0
 | 
						||
    });
 | 
						||
 | 
						||
    console.log(`Registered fundamentals provider: ${provider.name}`);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Unregister a price provider
 | 
						||
   */
 | 
						||
  public unregisterPriceProvider(providerName: string): void {
 | 
						||
    this.priceProviders.delete(providerName);
 | 
						||
    console.log(`Unregistered price provider: ${providerName}`);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Unregister a fundamentals provider
 | 
						||
   */
 | 
						||
  public unregisterFundamentalsProvider(providerName: string): void {
 | 
						||
    this.fundamentalsProviders.delete(providerName);
 | 
						||
    console.log(`Unregistered fundamentals provider: ${providerName}`);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get all registered price providers
 | 
						||
   */
 | 
						||
  public getPriceProviders(): IStockProvider[] {
 | 
						||
    return Array.from(this.priceProviders.values()).map(entry => entry.provider);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get all registered fundamentals providers
 | 
						||
   */
 | 
						||
  public getFundamentalsProviders(): IFundamentalsProvider[] {
 | 
						||
    return Array.from(this.fundamentalsProviders.values()).map(entry => entry.provider);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get enabled price providers sorted by priority
 | 
						||
   */
 | 
						||
  private getEnabledPriceProviders(): IStockProvider[] {
 | 
						||
    return Array.from(this.priceProviders.values())
 | 
						||
      .filter(entry => entry.config.enabled)
 | 
						||
      .sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
 | 
						||
      .map(entry => entry.provider);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get enabled fundamentals providers sorted by priority
 | 
						||
   */
 | 
						||
  private getEnabledFundamentalsProviders(): IFundamentalsProvider[] {
 | 
						||
    return Array.from(this.fundamentalsProviders.values())
 | 
						||
      .filter(entry => entry.config.enabled)
 | 
						||
      .sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0))
 | 
						||
      .map(entry => entry.provider);
 | 
						||
  }
 | 
						||
 | 
						||
  // ========== Data Fetching Methods ==========
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get current price for a single ticker
 | 
						||
   */
 | 
						||
  public async getPrice(ticker: string): Promise<IStockPrice> {
 | 
						||
    const cacheKey = `price:${ticker}`;
 | 
						||
    const cached = this.getFromCache(this.priceCache, cacheKey);
 | 
						||
 | 
						||
    if (cached) {
 | 
						||
      console.log(`Cache hit for price: ${ticker}`);
 | 
						||
      return cached as IStockPrice;
 | 
						||
    }
 | 
						||
 | 
						||
    const providers = this.getEnabledPriceProviders();
 | 
						||
    if (providers.length === 0) {
 | 
						||
      throw new Error('No price providers available');
 | 
						||
    }
 | 
						||
 | 
						||
    let lastError: Error | undefined;
 | 
						||
 | 
						||
    for (const provider of providers) {
 | 
						||
      const entry = this.priceProviders.get(provider.name)!;
 | 
						||
 | 
						||
      try {
 | 
						||
        const result = await this.fetchWithRetry(
 | 
						||
          () => provider.fetchData({ type: 'current', ticker }),
 | 
						||
          entry.config
 | 
						||
        );
 | 
						||
 | 
						||
        entry.successCount++;
 | 
						||
 | 
						||
        const price = result as IStockPrice;
 | 
						||
        this.addToCache(this.priceCache, cacheKey, price, this.config.cache.priceTTL);
 | 
						||
 | 
						||
        console.log(`Successfully fetched price for ${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 ${ticker}: ${error.message}`);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    throw new Error(
 | 
						||
      `Failed to fetch price for ${ticker} from all providers. Last error: ${lastError?.message}`
 | 
						||
    );
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get current prices for multiple tickers
 | 
						||
   */
 | 
						||
  public async getPrices(tickers: string[]): Promise<IStockPrice[]> {
 | 
						||
    const cacheKey = `prices:${tickers.sort().join(',')}`;
 | 
						||
    const cached = this.getFromCache(this.priceCache, cacheKey);
 | 
						||
 | 
						||
    if (cached) {
 | 
						||
      console.log(`Cache hit for prices: ${tickers.length} tickers`);
 | 
						||
      return cached as IStockPrice[];
 | 
						||
    }
 | 
						||
 | 
						||
    const providers = this.getEnabledPriceProviders();
 | 
						||
    if (providers.length === 0) {
 | 
						||
      throw new Error('No price providers available');
 | 
						||
    }
 | 
						||
 | 
						||
    let lastError: Error | undefined;
 | 
						||
 | 
						||
    for (const provider of providers) {
 | 
						||
      const entry = this.priceProviders.get(provider.name)!;
 | 
						||
 | 
						||
      try {
 | 
						||
        const result = await this.fetchWithRetry(
 | 
						||
          () => provider.fetchData({ type: 'batch', tickers }),
 | 
						||
          entry.config
 | 
						||
        );
 | 
						||
 | 
						||
        entry.successCount++;
 | 
						||
 | 
						||
        const prices = result as IStockPrice[];
 | 
						||
        this.addToCache(this.priceCache, cacheKey, prices, this.config.cache.priceTTL);
 | 
						||
 | 
						||
        console.log(`Successfully fetched ${prices.length} prices from ${provider.name}`);
 | 
						||
        return prices;
 | 
						||
      } 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 prices: ${error.message}`);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    throw new Error(
 | 
						||
      `Failed to fetch prices for ${tickers.length} tickers from all providers. Last error: ${lastError?.message}`
 | 
						||
    );
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get fundamentals for a single ticker
 | 
						||
   */
 | 
						||
  public async getFundamentals(ticker: string): Promise<IStockFundamentals> {
 | 
						||
    const cacheKey = `fundamentals:${ticker}`;
 | 
						||
    const cached = this.getFromCache(this.fundamentalsCache, cacheKey);
 | 
						||
 | 
						||
    if (cached) {
 | 
						||
      console.log(`Cache hit for fundamentals: ${ticker}`);
 | 
						||
      return cached as IStockFundamentals;
 | 
						||
    }
 | 
						||
 | 
						||
    const providers = this.getEnabledFundamentalsProviders();
 | 
						||
    if (providers.length === 0) {
 | 
						||
      throw new Error('No fundamentals providers available');
 | 
						||
    }
 | 
						||
 | 
						||
    let lastError: Error | undefined;
 | 
						||
 | 
						||
    for (const provider of providers) {
 | 
						||
      const entry = this.fundamentalsProviders.get(provider.name)!;
 | 
						||
 | 
						||
      try {
 | 
						||
        const result = await this.fetchWithRetry(
 | 
						||
          () => provider.fetchData({ type: 'fundamentals-current', ticker }),
 | 
						||
          entry.config
 | 
						||
        );
 | 
						||
 | 
						||
        entry.successCount++;
 | 
						||
 | 
						||
        const fundamentals = result as IStockFundamentals;
 | 
						||
        const ttl = (entry.config as IFundamentalsProviderConfig).cacheTTL || this.config.cache.fundamentalsTTL;
 | 
						||
        this.addToCache(this.fundamentalsCache, cacheKey, fundamentals, ttl);
 | 
						||
 | 
						||
        console.log(`Successfully fetched fundamentals for ${ticker} from ${provider.name}`);
 | 
						||
        return fundamentals;
 | 
						||
      } catch (error) {
 | 
						||
        entry.errorCount++;
 | 
						||
        entry.lastError = error as Error;
 | 
						||
        entry.lastErrorTime = new Date();
 | 
						||
        lastError = error as Error;
 | 
						||
 | 
						||
        console.warn(`Provider ${provider.name} failed for ${ticker} fundamentals: ${error.message}`);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    throw new Error(
 | 
						||
      `Failed to fetch fundamentals for ${ticker} from all providers. Last error: ${lastError?.message}`
 | 
						||
    );
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get fundamentals for multiple tickers
 | 
						||
   */
 | 
						||
  public async getBatchFundamentals(tickers: string[]): Promise<IStockFundamentals[]> {
 | 
						||
    const cacheKey = `fundamentals-batch:${tickers.sort().join(',')}`;
 | 
						||
    const cached = this.getFromCache(this.fundamentalsCache, cacheKey);
 | 
						||
 | 
						||
    if (cached) {
 | 
						||
      console.log(`Cache hit for batch fundamentals: ${tickers.length} tickers`);
 | 
						||
      return cached as IStockFundamentals[];
 | 
						||
    }
 | 
						||
 | 
						||
    const providers = this.getEnabledFundamentalsProviders();
 | 
						||
    if (providers.length === 0) {
 | 
						||
      throw new Error('No fundamentals providers available');
 | 
						||
    }
 | 
						||
 | 
						||
    let lastError: Error | undefined;
 | 
						||
 | 
						||
    for (const provider of providers) {
 | 
						||
      const entry = this.fundamentalsProviders.get(provider.name)!;
 | 
						||
 | 
						||
      try {
 | 
						||
        const result = await this.fetchWithRetry(
 | 
						||
          () => provider.fetchData({ type: 'fundamentals-batch', tickers }),
 | 
						||
          entry.config
 | 
						||
        );
 | 
						||
 | 
						||
        entry.successCount++;
 | 
						||
 | 
						||
        const fundamentals = result as IStockFundamentals[];
 | 
						||
        const ttl = (entry.config as IFundamentalsProviderConfig).cacheTTL || this.config.cache.fundamentalsTTL;
 | 
						||
        this.addToCache(this.fundamentalsCache, cacheKey, fundamentals, ttl);
 | 
						||
 | 
						||
        console.log(`Successfully fetched ${fundamentals.length} fundamentals from ${provider.name}`);
 | 
						||
        return fundamentals;
 | 
						||
      } 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 fundamentals: ${error.message}`);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    throw new Error(
 | 
						||
      `Failed to fetch fundamentals for ${tickers.length} tickers from all providers. Last error: ${lastError?.message}`
 | 
						||
    );
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * ✨ Get complete stock data (price + fundamentals) with automatic enrichment
 | 
						||
   */
 | 
						||
  public async getStockData(request: string | ICompleteStockDataRequest): Promise<IStockData> {
 | 
						||
    const normalizedRequest = typeof request === 'string'
 | 
						||
      ? { ticker: request, includeFundamentals: true, enrichFundamentals: true }
 | 
						||
      : { includeFundamentals: true, enrichFundamentals: true, ...request };
 | 
						||
 | 
						||
    const price = await this.getPrice(normalizedRequest.ticker);
 | 
						||
 | 
						||
    let fundamentals: IStockFundamentals | undefined;
 | 
						||
 | 
						||
    if (normalizedRequest.includeFundamentals) {
 | 
						||
      try {
 | 
						||
        fundamentals = await this.getFundamentals(normalizedRequest.ticker);
 | 
						||
 | 
						||
        // Enrich fundamentals with price calculations
 | 
						||
        if (normalizedRequest.enrichFundamentals && fundamentals) {
 | 
						||
          fundamentals = this.enrichWithPrice(fundamentals, price.price);
 | 
						||
        }
 | 
						||
      } catch (error) {
 | 
						||
        console.warn(`Failed to fetch fundamentals for ${normalizedRequest.ticker}: ${error.message}`);
 | 
						||
        // Continue without fundamentals
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    return {
 | 
						||
      ticker: normalizedRequest.ticker,
 | 
						||
      price,
 | 
						||
      fundamentals,
 | 
						||
      fetchedAt: new Date()
 | 
						||
    };
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * ✨ Get complete stock data for multiple tickers with automatic enrichment
 | 
						||
   */
 | 
						||
  public async getBatchStockData(request: string[] | ICompleteStockDataBatchRequest): Promise<IStockData[]> {
 | 
						||
    const normalizedRequest = Array.isArray(request)
 | 
						||
      ? { tickers: request, includeFundamentals: true, enrichFundamentals: true }
 | 
						||
      : { includeFundamentals: true, enrichFundamentals: true, ...request };
 | 
						||
 | 
						||
    const prices = await this.getPrices(normalizedRequest.tickers);
 | 
						||
    const priceMap = new Map(prices.map(p => [p.ticker, p]));
 | 
						||
 | 
						||
    let fundamentalsMap = new Map<string, IStockFundamentals>();
 | 
						||
 | 
						||
    if (normalizedRequest.includeFundamentals) {
 | 
						||
      try {
 | 
						||
        const fundamentals = await this.getBatchFundamentals(normalizedRequest.tickers);
 | 
						||
 | 
						||
        // Enrich with prices if requested
 | 
						||
        if (normalizedRequest.enrichFundamentals) {
 | 
						||
          for (const fund of fundamentals) {
 | 
						||
            const price = priceMap.get(fund.ticker);
 | 
						||
            if (price) {
 | 
						||
              fundamentalsMap.set(fund.ticker, this.enrichWithPrice(fund, price.price));
 | 
						||
            } else {
 | 
						||
              fundamentalsMap.set(fund.ticker, fund);
 | 
						||
            }
 | 
						||
          }
 | 
						||
        } else {
 | 
						||
          fundamentalsMap = new Map(fundamentals.map(f => [f.ticker, f]));
 | 
						||
        }
 | 
						||
      } catch (error) {
 | 
						||
        console.warn(`Failed to fetch batch fundamentals: ${error.message}`);
 | 
						||
        // Continue without fundamentals
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    return normalizedRequest.tickers.map(ticker => ({
 | 
						||
      ticker,
 | 
						||
      price: priceMap.get(ticker)!,
 | 
						||
      fundamentals: fundamentalsMap.get(ticker),
 | 
						||
      fetchedAt: new Date()
 | 
						||
    }));
 | 
						||
  }
 | 
						||
 | 
						||
  // ========== Helper Methods ==========
 | 
						||
 | 
						||
  /**
 | 
						||
   * Enrich fundamentals with calculated metrics using current price
 | 
						||
   */
 | 
						||
  private enrichWithPrice(fundamentals: IStockFundamentals, price: number): IStockFundamentals {
 | 
						||
    const enriched = { ...fundamentals };
 | 
						||
 | 
						||
    // Calculate market cap: price × shares outstanding
 | 
						||
    if (fundamentals.sharesOutstanding) {
 | 
						||
      enriched.marketCap = price * fundamentals.sharesOutstanding;
 | 
						||
    }
 | 
						||
 | 
						||
    // Calculate P/E ratio: price / EPS
 | 
						||
    if (fundamentals.earningsPerShareDiluted && fundamentals.earningsPerShareDiluted > 0) {
 | 
						||
      enriched.priceToEarnings = price / fundamentals.earningsPerShareDiluted;
 | 
						||
    }
 | 
						||
 | 
						||
    // Calculate price-to-book: market cap / stockholders equity
 | 
						||
    if (enriched.marketCap && fundamentals.stockholdersEquity && fundamentals.stockholdersEquity > 0) {
 | 
						||
      enriched.priceToBook = enriched.marketCap / fundamentals.stockholdersEquity;
 | 
						||
    }
 | 
						||
 | 
						||
    return enriched;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Fetch with retry logic
 | 
						||
   */
 | 
						||
  private async fetchWithRetry<T>(
 | 
						||
    fetchFn: () => Promise<T>,
 | 
						||
    config: IProviderConfig | IFundamentalsProviderConfig
 | 
						||
  ): Promise<T> {
 | 
						||
    const maxAttempts = config.retryAttempts || 1;
 | 
						||
    let lastError: Error | undefined;
 | 
						||
 | 
						||
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
 | 
						||
      try {
 | 
						||
        return await fetchFn();
 | 
						||
      } catch (error) {
 | 
						||
        lastError = error as Error;
 | 
						||
 | 
						||
        if (attempt < maxAttempts) {
 | 
						||
          const delay = (config.retryDelay || 1000) * attempt;
 | 
						||
          console.log(`Retry attempt ${attempt} after ${delay}ms`);
 | 
						||
          await plugins.smartdelay.delayFor(delay);
 | 
						||
        }
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    throw lastError || new Error('Unknown error during fetch');
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get from cache if not expired
 | 
						||
   */
 | 
						||
  private getFromCache<T>(cache: Map<string, ICacheEntry<T>>, key: string): T | null {
 | 
						||
    const entry = cache.get(key);
 | 
						||
 | 
						||
    if (!entry) {
 | 
						||
      return null;
 | 
						||
    }
 | 
						||
 | 
						||
    // Check if cache entry has expired
 | 
						||
    const age = Date.now() - entry.timestamp.getTime();
 | 
						||
    if (entry.ttl !== Infinity && age > entry.ttl) {
 | 
						||
      cache.delete(key);
 | 
						||
      return null;
 | 
						||
    }
 | 
						||
 | 
						||
    return entry.data;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Add to cache with TTL
 | 
						||
   */
 | 
						||
  private addToCache<T>(cache: Map<string, ICacheEntry<T>>, key: string, data: T, ttl: number): void {
 | 
						||
    // Enforce max entries limit
 | 
						||
    if (cache.size >= this.config.cache.maxEntries) {
 | 
						||
      // Remove oldest entry
 | 
						||
      const oldestKey = cache.keys().next().value;
 | 
						||
      if (oldestKey) {
 | 
						||
        cache.delete(oldestKey);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    cache.set(key, {
 | 
						||
      data,
 | 
						||
      timestamp: new Date(),
 | 
						||
      ttl
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  // ========== Health & Statistics ==========
 | 
						||
 | 
						||
  /**
 | 
						||
   * Check health of all providers (both price and fundamentals)
 | 
						||
   */
 | 
						||
  public async checkProvidersHealth(): Promise<Map<string, boolean>> {
 | 
						||
    const health = new Map<string, boolean>();
 | 
						||
 | 
						||
    // Check price providers
 | 
						||
    for (const [name, entry] of this.priceProviders) {
 | 
						||
      if (!entry.config.enabled) {
 | 
						||
        health.set(`${name} (price)`, false);
 | 
						||
        continue;
 | 
						||
      }
 | 
						||
 | 
						||
      try {
 | 
						||
        const isAvailable = await entry.provider.isAvailable();
 | 
						||
        health.set(`${name} (price)`, isAvailable);
 | 
						||
      } catch (error) {
 | 
						||
        health.set(`${name} (price)`, false);
 | 
						||
        console.error(`Health check failed for ${name}:`, error);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    // Check fundamentals providers
 | 
						||
    for (const [name, entry] of this.fundamentalsProviders) {
 | 
						||
      if (!entry.config.enabled) {
 | 
						||
        health.set(`${name} (fundamentals)`, false);
 | 
						||
        continue;
 | 
						||
      }
 | 
						||
 | 
						||
      try {
 | 
						||
        const isAvailable = await entry.provider.isAvailable();
 | 
						||
        health.set(`${name} (fundamentals)`, isAvailable);
 | 
						||
      } catch (error) {
 | 
						||
        health.set(`${name} (fundamentals)`, false);
 | 
						||
        console.error(`Health check failed for ${name}:`, error);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    return health;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get statistics for all providers
 | 
						||
   */
 | 
						||
  public getProviderStats(): Map<
 | 
						||
    string,
 | 
						||
    {
 | 
						||
      type: 'price' | 'fundamentals';
 | 
						||
      successCount: number;
 | 
						||
      errorCount: number;
 | 
						||
      lastError?: string;
 | 
						||
      lastErrorTime?: Date;
 | 
						||
    }
 | 
						||
  > {
 | 
						||
    const stats = new Map();
 | 
						||
 | 
						||
    // Price provider stats
 | 
						||
    for (const [name, entry] of this.priceProviders) {
 | 
						||
      stats.set(name, {
 | 
						||
        type: 'price',
 | 
						||
        successCount: entry.successCount,
 | 
						||
        errorCount: entry.errorCount,
 | 
						||
        lastError: entry.lastError?.message,
 | 
						||
        lastErrorTime: entry.lastErrorTime
 | 
						||
      });
 | 
						||
    }
 | 
						||
 | 
						||
    // Fundamentals provider stats
 | 
						||
    for (const [name, entry] of this.fundamentalsProviders) {
 | 
						||
      stats.set(name, {
 | 
						||
        type: 'fundamentals',
 | 
						||
        successCount: entry.successCount,
 | 
						||
        errorCount: entry.errorCount,
 | 
						||
        lastError: entry.lastError?.message,
 | 
						||
        lastErrorTime: entry.lastErrorTime
 | 
						||
      });
 | 
						||
    }
 | 
						||
 | 
						||
    return stats;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Clear all caches
 | 
						||
   */
 | 
						||
  public clearCache(): void {
 | 
						||
    this.priceCache.clear();
 | 
						||
    this.fundamentalsCache.clear();
 | 
						||
    console.log('All caches cleared');
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Get cache statistics
 | 
						||
   */
 | 
						||
  public getCacheStats(): {
 | 
						||
    priceCache: { size: number; ttl: number };
 | 
						||
    fundamentalsCache: { size: number; ttl: number };
 | 
						||
    maxEntries: number;
 | 
						||
  } {
 | 
						||
    return {
 | 
						||
      priceCache: {
 | 
						||
        size: this.priceCache.size,
 | 
						||
        ttl: this.config.cache.priceTTL
 | 
						||
      },
 | 
						||
      fundamentalsCache: {
 | 
						||
        size: this.fundamentalsCache.size,
 | 
						||
        ttl: this.config.cache.fundamentalsTTL
 | 
						||
      },
 | 
						||
      maxEntries: this.config.cache.maxEntries
 | 
						||
    };
 | 
						||
  }
 | 
						||
}
 |