|
|
|
@ -1,3 +1,4 @@ |
|
|
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; |
|
|
|
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; |
|
|
|
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; |
|
|
|
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; |
|
|
|
@ -21,8 +22,8 @@ import { |
|
|
|
|
|
|
|
import { Injectable, Logger } from '@nestjs/common'; |
|
|
|
import { DataSource, SymbolProfile } from '@prisma/client'; |
|
|
|
import { addDays, format, isSameDay } from 'date-fns'; |
|
|
|
import { getReasonPhrase, StatusCodes } from 'http-status-codes'; |
|
|
|
import { addDays, format, isSameDay, isValid } from 'date-fns'; |
|
|
|
import { ReasonPhrases, StatusCodes } from 'http-status-codes'; |
|
|
|
import { uniqBy } from 'lodash'; |
|
|
|
import YahooFinance from 'yahoo-finance2'; |
|
|
|
import { ChartResultArray } from 'yahoo-finance2/esm/src/modules/chart'; |
|
|
|
@ -45,6 +46,9 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
private static readonly DELISTED_ERROR_MESSAGE = |
|
|
|
'No data found, symbol may be delisted'; |
|
|
|
|
|
|
|
private static readonly RATE_LIMIT_ERROR_MESSAGE = |
|
|
|
ReasonPhrases.TOO_MANY_REQUESTS; |
|
|
|
|
|
|
|
private readonly logger = new Logger(YahooFinanceService.name); |
|
|
|
|
|
|
|
private readonly yahooFinance = new YahooFinance({ |
|
|
|
@ -52,6 +56,7 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
}); |
|
|
|
|
|
|
|
public constructor( |
|
|
|
private readonly configurationService: ConfigurationService, |
|
|
|
private readonly cryptocurrencyService: CryptocurrencyService, |
|
|
|
private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService |
|
|
|
) {} |
|
|
|
@ -78,9 +83,18 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
public async getDividends({ |
|
|
|
from, |
|
|
|
granularity = 'day', |
|
|
|
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), |
|
|
|
symbol, |
|
|
|
to |
|
|
|
}: GetDividendsParams) { |
|
|
|
if (!isValid(from) || !isValid(to)) { |
|
|
|
this.logger.error( |
|
|
|
`Could not get dividends for ${symbol} (${this.getName()}): Invalid date range` |
|
|
|
); |
|
|
|
|
|
|
|
return {}; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const historicalResult = this.convertToDividendResult( |
|
|
|
await this.yahooFinance.chart( |
|
|
|
@ -95,6 +109,9 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
isSameDay(from, to) ? addDays(to, 1) : to, |
|
|
|
DATE_FORMAT |
|
|
|
) |
|
|
|
}, |
|
|
|
{ |
|
|
|
fetchOptions: { signal: AbortSignal.timeout(requestTimeout) } |
|
|
|
} |
|
|
|
) |
|
|
|
); |
|
|
|
@ -113,16 +130,22 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
const message = `Could not get dividends for ${symbol} (${this.getName()}) from ${format( |
|
|
|
from, |
|
|
|
DATE_FORMAT |
|
|
|
)} to ${format(to, DATE_FORMAT)}: [${error?.name}] ${error?.message}`;
|
|
|
|
)} to ${format(to, DATE_FORMAT)}`;
|
|
|
|
|
|
|
|
if ( |
|
|
|
error?.code === StatusCodes.TOO_MANY_REQUESTS || |
|
|
|
error?.message === YahooFinanceService.DELISTED_ERROR_MESSAGE || |
|
|
|
error?.message?.includes(getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS)) |
|
|
|
if (error?.message === YahooFinanceService.DELISTED_ERROR_MESSAGE) { |
|
|
|
this.logger.warn( |
|
|
|
`${message}: ${YahooFinanceService.DELISTED_ERROR_MESSAGE}` |
|
|
|
); |
|
|
|
} else if ( |
|
|
|
(error?.name === 'HTTPError' && |
|
|
|
error?.code === StatusCodes.TOO_MANY_REQUESTS) || |
|
|
|
error?.message?.startsWith(YahooFinanceService.RATE_LIMIT_ERROR_MESSAGE) |
|
|
|
) { |
|
|
|
this.logger.warn(message); |
|
|
|
this.logger.warn( |
|
|
|
`${message}: ${YahooFinanceService.RATE_LIMIT_ERROR_MESSAGE}` |
|
|
|
); |
|
|
|
} else { |
|
|
|
this.logger.error(message); |
|
|
|
this.logger.error(`${message}: [${error?.name}] ${error?.message}`); |
|
|
|
} |
|
|
|
|
|
|
|
return {}; |
|
|
|
@ -131,6 +154,7 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
|
|
|
|
public async getHistorical({ |
|
|
|
from, |
|
|
|
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'), |
|
|
|
symbol, |
|
|
|
to |
|
|
|
}: GetHistoricalParams): Promise<{ |
|
|
|
@ -149,6 +173,9 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
isSameDay(from, to) ? addDays(to, 1) : to, |
|
|
|
DATE_FORMAT |
|
|
|
) |
|
|
|
}, |
|
|
|
{ |
|
|
|
fetchOptions: { signal: AbortSignal.timeout(requestTimeout) } |
|
|
|
} |
|
|
|
) |
|
|
|
); |
|
|
|
@ -165,7 +192,7 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
|
|
|
|
return response; |
|
|
|
} catch (error) { |
|
|
|
if (error.message === YahooFinanceService.DELISTED_ERROR_MESSAGE) { |
|
|
|
if (error?.message === YahooFinanceService.DELISTED_ERROR_MESSAGE) { |
|
|
|
throw new AssetProfileDelistedError( |
|
|
|
`No data found, ${symbol} (${this.getName()}) may be delisted` |
|
|
|
); |
|
|
|
@ -174,7 +201,7 @@ export class YahooFinanceService implements DataProviderInterface { |
|
|
|
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format( |
|
|
|
from, |
|
|
|
DATE_FORMAT |
|
|
|
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}` |
|
|
|
)} to ${format(to, DATE_FORMAT)}: [${error?.name}] ${error?.message}` |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|