BREAKING CHANGE(stocks): Unify stock provider API to discriminated IStockDataRequest and add company name/fullname enrichment
This commit is contained in:
11
changelog.md
11
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
|
||||
|
||||
|
||||
19
readme.md
19
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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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<IStockPrice> {
|
||||
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}`
|
||||
);
|
||||
}
|
||||
|
||||
public async getPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
|
||||
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
|
||||
/**
|
||||
* Convenience method: Get current price for a single ticker
|
||||
*/
|
||||
public async getPrice(request: ISimpleQuoteRequest): Promise<IStockPrice> {
|
||||
const result = await this.getData({
|
||||
type: 'current',
|
||||
ticker: request.ticker
|
||||
});
|
||||
const ttl = this.getCacheTTL(price.dataType);
|
||||
this.addToCache(cacheKey, price, ttl);
|
||||
return result as IStockPrice;
|
||||
}
|
||||
|
||||
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<IStockPrice[]> {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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<IStockPrice>;
|
||||
fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]>;
|
||||
fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]>;
|
||||
isAvailable(): Promise<boolean>;
|
||||
|
||||
supportsMarket?(market: string): boolean;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<IStockPrice> {
|
||||
// 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<IStockPrice[]> {
|
||||
// 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
|
||||
*/
|
||||
|
||||
@@ -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<IStockPrice> {
|
||||
/**
|
||||
* Unified data fetching method
|
||||
*/
|
||||
public async fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]> {
|
||||
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<IStockPrice> {
|
||||
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<IStockPrice[]> {
|
||||
/**
|
||||
* Fetch batch current prices
|
||||
*/
|
||||
private async fetchBatchCurrentPrices(request: IStockBatchCurrentRequest): Promise<IStockPrice[]> {
|
||||
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<boolean> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user