diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0751ba8..748de5d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added support for mutual funds +- Added the url to the symbol profile model + +### Changed + +- Migrated from `yahoo-finance` to `yahoo-finance2` + +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.120.0 - 25.02.2022 ### Changed diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index e1683c867..7d0f152b2 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -125,19 +125,19 @@ export class ImportService { } if (dataSource !== 'MANUAL') { - const result = await this.dataProviderService.get([ + const quotes = await this.dataProviderService.getQuotes([ { dataSource, symbol } ]); - if (result[symbol] === undefined) { + if (quotes[symbol] === undefined) { throw new Error( `orders.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` ); } - if (result[symbol].currency !== currency) { + if (quotes[symbol].currency !== currency) { throw new Error( - `orders.${index}.currency ("${currency}") does not match with "${result[symbol].currency}"` + `orders.${index}.currency ("${currency}") does not match with "${quotes[symbol].currency}"` ); } } diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index fac041837..0549596ce 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -40,7 +40,7 @@ export class CurrentRateService { const today = resetHours(new Date()); promises.push( this.dataProviderService - .get(dataGatheringItems) + .getQuotes(dataGatheringItems) .then((dataResultProvider) => { const result = []; for (const dataGatheringItem of dataGatheringItems) { diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index adeea5c91..99e9496fc 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -327,7 +327,7 @@ export class PortfolioServiceNew { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -358,7 +358,6 @@ export class PortfolioServiceNew { countries: symbolProfile.countries, currency: item.currency, dataSource: symbolProfile.dataSource, - exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, @@ -578,7 +577,7 @@ export class PortfolioServiceNew { ) }; } else { - const currentData = await this.dataProviderService.get([ + const currentData = await this.dataProviderService.getQuotes([ { dataSource: DataSource.YAHOO, symbol: aSymbol } ]); const marketPrice = currentData[aSymbol]?.marketPrice; @@ -679,7 +678,7 @@ export class PortfolioServiceNew { const symbols = positions.map((position) => position.symbol); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItem), + this.dataProviderService.getQuotes(dataGatheringItem), this.symbolProfileService.getSymbolProfiles(symbols) ]); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0a164708c..ca0c25b03 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -315,7 +315,7 @@ export class PortfolioService { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -346,7 +346,6 @@ export class PortfolioService { countries: symbolProfile.countries, currency: item.currency, dataSource: symbolProfile.dataSource, - exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, @@ -552,9 +551,10 @@ export class PortfolioService { SymbolProfile, transactionCount, averagePrice: averagePrice.toNumber(), - grossPerformancePercent: position.grossPerformancePercentage.toNumber(), + grossPerformancePercent: + position.grossPerformancePercentage?.toNumber(), historicalData: historicalDataArray, - netPerformancePercent: position.netPerformancePercentage.toNumber(), + netPerformancePercent: position.netPerformancePercentage?.toNumber(), quantity: quantity.toNumber(), value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice).toNumber(), @@ -563,7 +563,7 @@ export class PortfolioService { ) }; } else { - const currentData = await this.dataProviderService.get([ + const currentData = await this.dataProviderService.getQuotes([ { dataSource: DataSource.YAHOO, symbol: aSymbol } ]); const marketPrice = currentData[aSymbol]?.marketPrice; @@ -660,7 +660,7 @@ export class PortfolioService { const symbols = positions.map((position) => position.symbol); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItem), + this.dataProviderService.getQuotes(dataGatheringItem), this.symbolProfileService.getSymbolProfiles(symbols) ]); diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 37b1c5864..8d73617c6 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -27,8 +27,10 @@ export class SymbolService { dataGatheringItem: IDataGatheringItem; includeHistoricalData?: number; }): Promise { - const response = await this.dataProviderService.get([dataGatheringItem]); - const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {}; + const quotes = await this.dataProviderService.getQuotes([ + dataGatheringItem + ]); + const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {}; if (dataGatheringItem.dataSource && marketPrice) { let historicalData: HistoricalDataItem[] = []; diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 81c9c884d..f1757e462 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -220,32 +220,41 @@ export class DataGatheringService { Logger.log('Profile data gathering has been started.'); console.time('data-gathering-profile'); - let dataGatheringItems = aDataGatheringItems; + let dataGatheringItems = aDataGatheringItems?.filter( + (dataGatheringItem) => { + return dataGatheringItem.dataSource !== 'MANUAL'; + } + ); if (!dataGatheringItems) { dataGatheringItems = await this.getSymbolsProfileData(); } - const currentData = await this.dataProviderService.get(dataGatheringItems); + const assetProfiles = await this.dataProviderService.getAssetProfiles( + dataGatheringItems + ); const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( dataGatheringItems.map(({ symbol }) => { return symbol; }) ); - for (const [symbol, response] of Object.entries(currentData)) { + for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { const symbolMapping = symbolProfiles.find((symbolProfile) => { return symbolProfile.symbol === symbol; })?.symbolMapping; for (const dataEnhancer of this.dataEnhancers) { try { - currentData[symbol] = await dataEnhancer.enhance({ - response, + assetProfiles[symbol] = await dataEnhancer.enhance({ + response: assetProfile, symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol }); } catch (error) { - Logger.error(`Failed to enhance data for symbol ${symbol}`, error); + Logger.error( + `Failed to enhance data for symbol ${symbol} by ${dataEnhancer.getName()}`, + error + ); } } @@ -256,8 +265,9 @@ export class DataGatheringService { currency, dataSource, name, - sectors - } = currentData[symbol]; + sectors, + url + } = assetProfiles[symbol]; try { await this.prismaService.symbolProfile.upsert({ @@ -269,7 +279,8 @@ export class DataGatheringService { dataSource, name, sectors, - symbol + symbol, + url }, update: { assetClass, @@ -277,7 +288,8 @@ export class DataGatheringService { countries, currency, name, - sectors + sectors, + url }, where: { dataSource_symbol: { @@ -300,6 +312,10 @@ export class DataGatheringService { let symbolCounter = 0; for (const { dataSource, date, symbol } of aSymbolsWithStartDate) { + if (dataSource === 'MANUAL') { + continue; + } + this.dataGatheringProgress = symbolCounter / aSymbolsWithStartDate.length; try { @@ -347,7 +363,7 @@ export class DataGatheringService { } catch {} } else { Logger.warn( - `Failed to gather data for symbol ${symbol} at ${format( + `Failed to gather data for symbol ${symbol} from ${dataSource} at ${format( currentDate, DATE_FORMAT )}.` diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 838f1ae6e..c1c0583d9 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -1,15 +1,15 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import { isAfter, isBefore, parse } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; @@ -29,25 +29,23 @@ export class AlphaVantageService implements DataProviderInterface { return !!this.configurationService.get('ALPHA_VANTAGE_API_KEY'); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - - const symbol = aSymbols[0]; + const symbol = aSymbol; try { const historicalData: { @@ -88,6 +86,12 @@ export class AlphaVantageService implements DataProviderInterface { return DataSource.ALPHA_VANTAGE; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const result = await this.alphaVantage.data.search(aQuery); diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index a469e57a5..f61297368 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -1,5 +1,7 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; -import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { Country } from '@ghostfolio/common/interfaces/country.interface'; +import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; +import { SymbolProfile } from '@prisma/client'; import bent from 'bent'; const getJSON = bent('json'); @@ -21,9 +23,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { response, symbol }: { - response: IDataProviderResponse; + response: Partial; symbol: string; - }): Promise { + }): Promise> { if ( !(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF') ) { @@ -40,7 +42,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { ); }); - if (!response.countries || response.countries.length === 0) { + if ( + !response.countries || + (response.countries as unknown as Country[]).length === 0 + ) { response.countries = []; for (const [name, value] of Object.entries(holdings.countries)) { let countryCode: string; @@ -65,7 +70,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { } } - if (!response.sectors || response.sectors.length === 0) { + if ( + !response.sectors || + (response.sectors as unknown as Sector[]).length === 0 + ) { response.sectors = []; for (const [name, value] of Object.entries(holdings.sectors)) { response.sectors.push({ diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 6ffd5b2dd..71cc293d4 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -10,7 +10,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { format, isValid } from 'date-fns'; import { groupBy, isEmpty } from 'lodash'; @@ -23,42 +23,6 @@ export class DataProviderService { private readonly prismaService: PrismaService ) {} - public async get(items: IDataGatheringItem[]): Promise<{ - [symbol: string]: IDataProviderResponse; - }> { - const response: { - [symbol: string]: IDataProviderResponse; - } = {}; - - const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); - - const promises = []; - - for (const [dataSource, dataGatheringItems] of Object.entries( - itemsGroupedByDataSource - )) { - const symbols = dataGatheringItems.map((dataGatheringItem) => { - return dataGatheringItem.symbol; - }); - - const promise = Promise.resolve( - this.getDataProvider(DataSource[dataSource]).get(symbols) - ); - - promises.push( - promise.then((result) => { - for (const [symbol, dataProviderResponse] of Object.entries(result)) { - response[symbol] = dataProviderResponse; - } - }) - ); - } - - await Promise.all(promises); - - return response; - } - public async getHistorical( aItems: IDataGatheringItem[], aGranularity: Granularity = 'month', @@ -144,7 +108,7 @@ export class DataProviderService { if (dataProvider.canHandle(symbol)) { promises.push( dataProvider - .getHistorical([symbol], undefined, from, to) + .getHistorical(symbol, undefined, from, to) .then((data) => ({ data: data?.[symbol], symbol })) ); } @@ -158,6 +122,82 @@ export class DataProviderService { return result; } + public getPrimaryDataSource(): DataSource { + return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; + } + + public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{ + [symbol: string]: Partial; + }> { + const response: { + [symbol: string]: Partial; + } = {}; + + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); + + const promises = []; + + for (const [dataSource, dataGatheringItems] of Object.entries( + itemsGroupedByDataSource + )) { + const symbols = dataGatheringItems.map((dataGatheringItem) => { + return dataGatheringItem.symbol; + }); + + for (const symbol of symbols) { + const promise = Promise.resolve( + this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol) + ); + + promises.push( + promise.then((symbolProfile) => { + response[symbol] = symbolProfile; + }) + ); + } + } + + await Promise.all(promises); + + return response; + } + + public async getQuotes(items: IDataGatheringItem[]): Promise<{ + [symbol: string]: IDataProviderResponse; + }> { + const response: { + [symbol: string]: IDataProviderResponse; + } = {}; + + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); + + const promises = []; + + for (const [dataSource, dataGatheringItems] of Object.entries( + itemsGroupedByDataSource + )) { + const symbols = dataGatheringItems.map((dataGatheringItem) => { + return dataGatheringItem.symbol; + }); + + const promise = Promise.resolve( + this.getDataProvider(DataSource[dataSource]).getQuotes(symbols) + ); + + promises.push( + promise.then((result) => { + for (const [symbol, dataProviderResponse] of Object.entries(result)) { + response[symbol] = dataProviderResponse; + } + }) + ); + } + + await Promise.all(promises); + + return response; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const promises: Promise<{ items: LookupItem[] }>[] = []; let lookupItems: LookupItem[] = []; @@ -184,10 +224,6 @@ export class DataProviderService { }; } - public getPrimaryDataSource(): DataSource { - return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; - } - private getDataProvider(providerName: DataSource) { for (const dataProviderInterface of this.dataProviderInterfaces) { if (dataProviderInterface.getName() === providerName) { diff --git a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts index a34f7cf92..dbc7dc97c 100644 --- a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts @@ -14,7 +14,7 @@ import { } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import * as cheerio from 'cheerio'; import { format } from 'date-fns'; @@ -32,57 +32,25 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return isGhostfolioScraperApiSymbol(symbol); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const [symbol] = aSymbols; - const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( - [symbol] - ); - - const { marketPrice } = await this.prismaService.marketData.findFirst({ - orderBy: { - date: 'desc' - }, - where: { - symbol - } - }); - - return { - [symbol]: { - marketPrice, - currency: symbolProfile?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed - } - }; - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - try { - const [symbol] = aSymbols; + const symbol = aSymbol; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( [symbol] ); @@ -115,6 +83,43 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return DataSource.GHOSTFOLIO; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const [symbol] = aSymbols; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( + [symbol] + ); + + const { marketPrice } = await this.prismaService.marketData.findFirst({ + orderBy: { + date: 'desc' + }, + where: { + symbol + } + }); + + return { + [symbol]: { + marketPrice, + currency: symbolProfile?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + } + }; + } catch (error) { + Logger.error(error); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items = await this.prismaService.symbolProfile.findMany({ select: { diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index fff9db21e..3bc427fd3 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -11,7 +11,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import { format } from 'date-fns'; import { GoogleSpreadsheet } from 'google-spreadsheet'; @@ -27,65 +27,24 @@ export class GoogleSheetsService implements DataProviderInterface { return true; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const response: { [symbol: string]: IDataProviderResponse } = {}; - - const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( - aSymbols - ); - - const sheet = await this.getSheet({ - sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), - symbol: 'Overview' - }); - - const rows = await sheet.getRows(); - - for (const row of rows) { - const marketPrice = parseFloat(row['marketPrice']); - const symbol = row['symbol']; - - if (aSymbols.includes(symbol)) { - response[symbol] = { - marketPrice, - currency: symbolProfiles.find((symbolProfile) => { - return symbolProfile.symbol === symbol; - })?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed - }; - } - } - - return response; - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - try { - const [symbol] = aSymbols; + const symbol = aSymbol; const sheet = await this.getSheet({ symbol, @@ -123,6 +82,51 @@ export class GoogleSheetsService implements DataProviderInterface { return DataSource.GOOGLE_SHEETS; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const response: { [symbol: string]: IDataProviderResponse } = {}; + + const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( + aSymbols + ); + + const sheet = await this.getSheet({ + sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), + symbol: 'Overview' + }); + + const rows = await sheet.getRows(); + + for (const row of rows) { + const marketPrice = parseFloat(row['marketPrice']); + const symbol = row['symbol']; + + if (aSymbols.includes(symbol)) { + response[symbol] = { + marketPrice, + currency: symbolProfiles.find((symbolProfile) => { + return symbolProfile.symbol === symbol; + })?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + }; + } + } + + return response; + } catch (error) { + Logger.error(error); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items = await this.prismaService.symbolProfile.findMany({ select: { diff --git a/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts b/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts index 26585b320..4e5ce8cba 100644 --- a/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts @@ -1,13 +1,13 @@ -import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { SymbolProfile } from '@prisma/client'; export interface DataEnhancerInterface { enhance({ response, symbol }: { - response: IDataProviderResponse; + response: Partial; symbol: string; - }): Promise; + }): Promise>; getName(): string; } diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts index c5cf4c330..16cf44603 100644 --- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts @@ -4,23 +4,27 @@ import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { Granularity } from '@ghostfolio/common/types'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; export interface DataProviderInterface { canHandle(symbol: string): boolean; - get(aSymbols: string[]): Promise<{ [symbol: string]: IDataProviderResponse }>; + getAssetProfile(aSymbol: string): Promise>; getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity, from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; - }>; + }>; // TODO: Return only one symbol getName(): DataSource; + getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }>; + search(aQuery: string): Promise<{ items: LookupItem[] }>; } diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index 3a486f897..edcdd2cde 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -6,7 +6,7 @@ import { } from '@ghostfolio/api/services/interfaces/interfaces'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; @Injectable() export class ManualService implements DataProviderInterface { @@ -16,14 +16,16 @@ export class ManualService implements DataProviderInterface { return false; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date @@ -37,6 +39,12 @@ export class ManualService implements DataProviderInterface { return DataSource.MANUAL; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } diff --git a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts index bdfc147dd..47f7eba40 100644 --- a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts @@ -1,19 +1,19 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { DATE_FORMAT, getToday, getYesterday } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import { format, subMonths, subWeeks, subYears } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; @Injectable() @@ -29,50 +29,24 @@ export class RakutenRapidApiService implements DataProviderInterface { return !!this.configurationService.get('RAKUTEN_RAPID_API_KEY'); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const symbol = aSymbols[0]; - - if (symbol === ghostfolioFearAndGreedIndexSymbol) { - const fgi = await this.getFearAndGreedIndex(); - - return { - [ghostfolioFearAndGreedIndexSymbol]: { - currency: undefined, - dataSource: this.getName(), - marketPrice: fgi.now.value, - marketState: MarketState.open, - name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME - } - }; - } - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - try { - const symbol = aSymbols[0]; + const symbol = aSymbol; if (symbol === ghostfolioFearAndGreedIndexSymbol) { const fgi = await this.getFearAndGreedIndex(); @@ -129,6 +103,35 @@ export class RakutenRapidApiService implements DataProviderInterface { return DataSource.RAKUTEN; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const symbol = aSymbols[0]; + + if (symbol === ghostfolioFearAndGreedIndexSymbol) { + const fgi = await this.getFearAndGreedIndex(); + + return { + [ghostfolioFearAndGreedIndexSymbol]: { + currency: undefined, + dataSource: this.getName(), + marketPrice: fgi.now.value, + marketState: MarketState.open + } + }; + } + } catch (error) { + Logger.error(error); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } diff --git a/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts b/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts deleted file mode 100644 index d41a43d39..000000000 --- a/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface IYahooFinanceHistoricalResponse { - adjClose: number; - close: number; - date: Date; - high: number; - low: number; - open: number; - symbol: string; - volume: number; -} - -export interface IYahooFinanceQuoteResponse { - price: IYahooFinancePrice; - summaryProfile: IYahooFinanceSummaryProfile; -} - -export interface IYahooFinancePrice { - currency: string; - exchangeName: string; - longName: string; - marketState: string; - quoteType: string; - regularMarketPrice: number; - shortName: string; -} - -export interface IYahooFinanceSummaryProfile { - country?: string; - industry?: string; - sector?: string; - website?: string; -} diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 556cf71a1..e7fb7e820 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -1,27 +1,27 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; -import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { + AssetClass, + AssetSubClass, + DataSource, + SymbolProfile +} from '@prisma/client'; import * as bent from 'bent'; import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; -import * as yahooFinance from 'yahoo-finance'; +import yahooFinance2 from 'yahoo-finance2'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; -import { - IYahooFinanceHistoricalResponse, - IYahooFinancePrice, - IYahooFinanceQuoteResponse -} from './interfaces/interfaces'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -73,145 +73,113 @@ export class YahooFinanceService implements DataProviderInterface { return aSymbol; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - const yahooFinanceSymbols = aSymbols.map((symbol) => - this.convertToYahooFinanceSymbol(symbol) - ); + public async getAssetProfile( + aSymbol: string + ): Promise> { + const response: Partial = {}; try { - const response: { [symbol: string]: IDataProviderResponse } = {}; - - const data: { - [symbol: string]: IYahooFinanceQuoteResponse; - } = await yahooFinance.quote({ - modules: ['price', 'summaryProfile'], - symbols: yahooFinanceSymbols + const symbol = this.convertToYahooFinanceSymbol(aSymbol); + const assetProfile = await yahooFinance2.quoteSummary(symbol, { + modules: ['price', 'summaryProfile'] }); - for (const [yahooFinanceSymbol, value] of Object.entries(data)) { - // Convert symbols back - const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - - const { assetClass, assetSubClass } = this.parseAssetClass(value.price); + const { assetClass, assetSubClass } = this.parseAssetClass( + assetProfile.price + ); - response[symbol] = { - assetClass, - assetSubClass, - currency: value.price?.currency, - dataSource: this.getName(), - exchange: this.parseExchange(value.price?.exchangeName), - marketState: - value.price?.marketState === 'REGULAR' || - this.cryptocurrencyService.isCryptocurrency(symbol) - ? MarketState.open - : MarketState.closed, - marketPrice: value.price?.regularMarketPrice || 0, - name: value.price?.longName || value.price?.shortName || symbol - }; + response.assetClass = assetClass; + response.assetSubClass = assetSubClass; + response.currency = assetProfile.price.currency; + response.dataSource = this.getName(); + response.name = + assetProfile.price.longName || assetProfile.price.shortName || symbol; + response.symbol = aSymbol; + + if ( + assetSubClass === AssetSubClass.STOCK && + assetProfile.summaryProfile?.country + ) { + // Add country if asset is stock and country available - if (value.price?.currency === 'GBp') { - // Convert GBp (pence) to GBP - response[symbol].currency = 'GBP'; - response[symbol].marketPrice = new Big( - value.price?.regularMarketPrice ?? 0 - ) - .div(100) - .toNumber(); - } + try { + const [code] = Object.entries(countries).find(([, country]) => { + return country.name === assetProfile.summaryProfile?.country; + }); - // Add country if stock and available - if ( - assetSubClass === AssetSubClass.STOCK && - value.summaryProfile?.country - ) { - try { - const [code] = Object.entries(countries).find(([, country]) => { - return country.name === value.summaryProfile?.country; - }); - - if (code) { - response[symbol].countries = [{ code, weight: 1 }]; - } - } catch {} - - if (value.summaryProfile?.sector) { - response[symbol].sectors = [ - { name: value.summaryProfile?.sector, weight: 1 } - ]; + if (code) { + response.countries = [{ code, weight: 1 }]; } - } + } catch {} - // Add url if available - const url = value.summaryProfile?.website; - if (url) { - response[symbol].url = url; + if (assetProfile.summaryProfile?.sector) { + response.sectors = [ + { name: assetProfile.summaryProfile?.sector, weight: 1 } + ]; } } - return response; - } catch (error) { - Logger.error(error); + const url = assetProfile.summaryProfile?.website; + if (url) { + response.url = url; + } + } catch {} - return {}; - } + return response; } public async getHistorical( - aSymbols: string[], + aSymbol: string, aGranularity: Granularity = 'day', from: Date, to: Date ): Promise<{ [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; }> { - if (aSymbols.length <= 0) { - return {}; - } - if (isSameDay(from, to)) { to = addDays(to, 1); } - const yahooFinanceSymbols = aSymbols.map((symbol) => { - return this.convertToYahooFinanceSymbol(symbol); - }); + const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol); try { - const historicalData: { - [symbol: string]: IYahooFinanceHistoricalResponse[]; - } = await yahooFinance.historical({ - symbols: yahooFinanceSymbols, - from: format(from, DATE_FORMAT), - to: format(to, DATE_FORMAT) - }); + const historicalResult = await yahooFinance2.historical( + yahooFinanceSymbol, + { + interval: '1d', + period1: format(from, DATE_FORMAT), + period2: format(to, DATE_FORMAT) + } + ); const response: { [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; } = {}; - for (const [yahooFinanceSymbol, timeSeries] of Object.entries( - historicalData - )) { - // Convert symbols back - const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - response[symbol] = {}; + // Convert symbol back + const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - timeSeries.forEach((timeSerie) => { - response[symbol][format(timeSerie.date, DATE_FORMAT)] = { - marketPrice: timeSerie.close, - performance: timeSerie.open - timeSerie.close - }; - }); + response[symbol] = {}; + + for (const historicalItem of historicalResult) { + let marketPrice = historicalItem.close; + + if (symbol === 'USDGBp') { + // Convert GPB to GBp (pence) + marketPrice = new Big(marketPrice).mul(100).toNumber(); + } + + response[symbol][format(historicalItem.date, DATE_FORMAT)] = { + marketPrice, + performance: historicalItem.open - historicalItem.close + }; } return response; } catch (error) { - Logger.error(error); + Logger.warn( + `Skipping yahooFinance2.getHistorical("${aSymbol}"): [${error.name}] ${error.message}` + ); return {}; } @@ -221,6 +189,56 @@ export class YahooFinanceService implements DataProviderInterface { return DataSource.YAHOO; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + const yahooFinanceSymbols = aSymbols.map((symbol) => + this.convertToYahooFinanceSymbol(symbol) + ); + + try { + const response: { [symbol: string]: IDataProviderResponse } = {}; + + const quotes = await yahooFinance2.quote(yahooFinanceSymbols); + + for (const quote of quotes) { + // Convert symbols back + const symbol = this.convertFromYahooFinanceSymbol(quote.symbol); + + response[symbol] = { + currency: quote.currency, + dataSource: this.getName(), + marketState: + quote.marketState === 'REGULAR' || + this.cryptocurrencyService.isCryptocurrency(symbol) + ? MarketState.open + : MarketState.closed, + marketPrice: quote.regularMarketPrice || 0 + }; + + if (symbol === 'USDGBP' && yahooFinanceSymbols.includes('USDGBp=X')) { + // Convert GPB to GBp (pence) + response['USDGBp'] = { + ...response[symbol], + currency: 'GBp', + marketPrice: new Big(response[symbol].marketPrice) + .mul(100) + .toNumber() + }; + } + } + + return response; + } catch (error) { + Logger.error(error); + + return {}; + } + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items: LookupItem[] = []; @@ -236,7 +254,7 @@ export class YahooFinanceService implements DataProviderInterface { const searchResult = await get(); - const symbols: string[] = searchResult.quotes + const quotes = searchResult.quotes .filter((quote) => { // filter out undefined symbols return quote.symbol; @@ -247,8 +265,7 @@ export class YahooFinanceService implements DataProviderInterface { this.cryptocurrencyService.isCryptocurrency( symbol.replace(new RegExp(`-${baseCurrency}$`), baseCurrency) )) || - quoteType === 'EQUITY' || - quoteType === 'ETF' + ['EQUITY', 'ETF', 'MUTUALFUND'].includes(quoteType) ); }) .filter(({ quoteType, symbol }) => { @@ -259,19 +276,24 @@ export class YahooFinanceService implements DataProviderInterface { } return true; - }) - .map(({ symbol }) => { - return symbol; }); - const marketData = await this.get(symbols); + const marketData = await this.getQuotes( + quotes.map(({ symbol }) => { + return symbol; + }) + ); for (const [symbol, value] of Object.entries(marketData)) { + const quote = quotes.find((currentQuote: any) => { + return currentQuote.symbol === symbol; + }); + items.push({ symbol, currency: value.currency, dataSource: this.getName(), - name: value.name + name: quote?.longname || quote?.shortname || symbol }); } } catch (error) { @@ -281,7 +303,7 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private parseAssetClass(aPrice: IYahooFinancePrice): { + private parseAssetClass(aPrice: any): { assetClass: AssetClass; assetSubClass: AssetSubClass; } { @@ -301,16 +323,12 @@ export class YahooFinanceService implements DataProviderInterface { assetClass = AssetClass.EQUITY; assetSubClass = AssetSubClass.ETF; break; + case 'mutualfund': + assetClass = AssetClass.EQUITY; + assetSubClass = AssetSubClass.MUTUALFUND; + break; } return { assetClass, assetSubClass }; } - - private parseExchange(aString: string): string { - if (aString?.toLowerCase() === 'ccc') { - return UNKNOWN_KEY; - } - - return aString; - } } diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index f77f7ef79..0770cf0c4 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -2,7 +2,7 @@ import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; import { format } from 'date-fns'; -import { isEmpty, isNumber, uniq } from 'lodash'; +import { isNumber, uniq } from 'lodash'; import { DataProviderService } from './data-provider/data-provider.service'; import { IDataGatheringItem } from './interfaces/interfaces'; @@ -61,7 +61,7 @@ export class ExchangeRateDataService { if (Object.keys(result).length !== this.currencyPairs.length) { // Load currencies directly from data provider as a fallback // if historical data is not fully available - const historicalData = await this.dataProviderService.get( + const historicalData = await this.dataProviderService.getQuotes( this.currencyPairs.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }) diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index c7d3a08f7..50fd6009f 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -33,19 +33,10 @@ export interface IDataProviderHistoricalResponse { } export interface IDataProviderResponse { - assetClass?: AssetClass; - assetSubClass?: AssetSubClass; - countries?: { code: string; weight: number }[]; currency: string; dataSource: DataSource; - exchange?: string; - marketChange?: number; - marketChangePercent?: number; marketPrice: number; marketState: MarketState; - name?: string; - sectors?: { name: string; weight: number }[]; - url?: string; } export interface IDataGatheringItem { diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index 8d254bb98..d846ecd43 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -39,11 +39,6 @@
{{ position?.name }}
{{ position?.symbol | gfSymbol }} - ({{ position.exchange }})
= 2.9.0" -"moment@>= 2.9.0", moment@^2.17.1, moment@^2.27.0: +"moment@>= 2.9.0", moment@^2.27.0: version "2.29.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -13985,11 +13995,6 @@ nx@13.8.1: dependencies: "@nrwl/cli" "13.8.1" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - oauth@0.9.x: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" @@ -15952,49 +15957,6 @@ request-progress@^3.0.0: dependencies: throttleit "^1.0.0" -request-promise-core@1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" - integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== - dependencies: - lodash "^4.17.19" - -request-promise@^4.2.1: - version "4.2.6" - resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.6.tgz#7e7e5b9578630e6f598e3813c0f8eb342a27f0a2" - integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ== - dependencies: - bluebird "^3.5.0" - request-promise-core "1.1.4" - stealthy-require "^1.1.1" - tough-cookie "^2.3.3" - -request@^2.79.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -16884,11 +16846,6 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stealthy-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" - integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= - store2@^2.12.0: version "2.12.0" resolved "https://registry.yarnpkg.com/store2/-/store2-2.12.0.tgz#e1f1b7e1a59b6083b2596a8d067f6ee88fd4d3cf" @@ -17031,11 +16988,6 @@ string.prototype.trimstart@^1.0.4: call-bind "^1.0.2" define-properties "^1.1.3" -string@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/string/-/string-3.3.3.tgz#5ea211cd92d228e184294990a6cc97b366a77cb0" - integrity sha1-XqIRzZLSKOGEKUmQpsyXs2anfLA= - string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -17526,14 +17478,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@^2.3.2, tough-cookie@^2.3.3, tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" @@ -17543,6 +17487,14 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -18764,20 +18716,14 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yahoo-finance@0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/yahoo-finance/-/yahoo-finance-0.3.6.tgz#c99fe8ff6c9a80babbb7e75881a244a862f6739f" - integrity sha512-SyXGhtvJvoU8E7XQJzviCBeuJNAMZoERJLfWwAERfDDgoPCu3/zBDDDt7l8hp3HmtIygLpqGuRJ7jzkip2AcZA== +yahoo-finance2@2.1.9: + version "2.1.9" + resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.1.9.tgz#28b157e1cddc5b56e6b354f6b00b453a41bbe8a4" + integrity sha512-xLlDqcbK+4Y4oSV7Vq1KcvNcjMuODHQrk2uLyBR4SlXDNjRV7XFpTrwMrDnSLu4pErenj0gXG3ARiCWidFjqzg== dependencies: - bluebird "^3.4.6" - debug "^2.3.3" - lodash "^4.17.2" - moment "^2.17.1" - moment-timezone "^0.5.10" - request "^2.79.0" - request-promise "^4.2.1" - string "^3.3.3" - tough-cookie "^2.3.2" + ajv "8.10.0" + ajv-formats "2.1.1" + node-fetch "^2.6.1" yallist@^3.0.2: version "3.1.1"