@ -1,27 +1,27 @@
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface' ;
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface' ;
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service' ;
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 { DATE_FORMAT , isCurrency } from '@ghostfolio/common/helper' ;
import { Granularity } from '@ghostfolio/common/types' ;
import { Granularity } from '@ghostfolio/common/types' ;
import { Injectable , Logger } from '@nestjs/common' ;
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 * as bent from 'bent' ;
import Big from 'big.js' ;
import Big from 'big.js' ;
import { countries } from 'countries-list' ;
import { countries } from 'countries-list' ;
import { addDays , format , isSameDay } from 'date-fns' ;
import { addDays , format , isSameDay } from 'date-fns' ;
import * as yahooFinance from 'yahoo-finance' ;
import yahooFinance 2 from 'yahoo-finance 2 ';
import {
IDataProviderHistoricalResponse ,
IDataProviderResponse ,
MarketState
} from '../../interfaces/interfaces' ;
import { DataProviderInterface } from '../interfaces/data-provider.interface' ;
import { DataProviderInterface } from '../interfaces/data-provider.interface' ;
import {
IYahooFinanceHistoricalResponse ,
IYahooFinancePrice ,
IYahooFinanceQuoteResponse
} from './interfaces/interfaces' ;
@Injectable ( )
@Injectable ( )
export class YahooFinanceService implements DataProviderInterface {
export class YahooFinanceService implements DataProviderInterface {
@ -73,145 +73,113 @@ export class YahooFinanceService implements DataProviderInterface {
return aSymbol ;
return aSymbol ;
}
}
public async get (
public async getAssetProfile (
aSymbols : string [ ]
aSymbol : string
) : Promise < { [ symbol : string ] : IDataProviderResponse } > {
) : Promise < Partial < SymbolProfile > > {
if ( aSymbols . length <= 0 ) {
const response : Partial < SymbolProfile > = { } ;
return { } ;
}
const yahooFinanceSymbols = aSymbols . map ( ( symbol ) = >
this . convertToYahooFinanceSymbol ( symbol )
) ;
try {
try {
const response : { [ symbol : string ] : IDataProviderResponse } = { } ;
const symbol = this . convertToYahooFinanceSymbol ( aSymbol ) ;
const assetProfile = await yahooFinance2 . quoteSummary ( symbol , {
const data : {
module s : [ 'price' , 'summaryProfile' ]
[ symbol : string ] : IYahooFinanceQuoteResponse ;
} = await yahooFinance . quote ( {
module s : [ 'price' , 'summaryProfile' ] ,
symbols : yahooFinanceSymbols
} ) ;
} ) ;
for ( const [ yahooFinanceSymbol , value ] of Object . entries ( data ) ) {
const { assetClass , assetSubClass } = this . parseAssetClass (
// Convert symbols back
assetProfile . price
const symbol = this . convertFromYahooFinanceSymbol ( yahooFinanceSymbol ) ;
) ;
const { assetClass , assetSubClass } = this . parseAssetClass ( value . price ) ;
response [ symbol ] = {
response . assetClass = assetClass ;
assetClass ,
response . assetSubClass = assetSubClass ;
assetSubClass ,
response . currency = assetProfile . price . currency ;
currency : value.price?.currency ,
response . dataSource = this . getName ( ) ;
dataSource : this.getName ( ) ,
response . name =
exchange : this.parseExchange ( value . price ? . exchangeName ) ,
assetProfile . price . longName || assetProfile . price . shortName || symbol ;
marketState :
response . symbol = aSymbol ;
value . price ? . marketState === 'REGULAR' ||
this . cryptocurrencyService . isCryptocurrency ( symbol )
if (
? MarketState . open
assetSubClass === AssetSubClass . STOCK &&
: MarketState . closed ,
assetProfile . summaryProfile ? . country
marketPrice : value.price?.regularMarketPrice || 0 ,
) {
name : value.price?.longName || value . price ? . shortName || symbol
// Add country if asset is stock and country available
} ;
if ( value . price ? . currency === 'GBp' ) {
try {
// Convert GBp (pence) to GBP
const [ code ] = Object . entries ( countries ) . find ( ( [ , country ] ) = > {
response [ symbol ] . currency = 'GBP' ;
return country . name === assetProfile . summaryProfile ? . country ;
response [ symbol ] . marketPrice = new Big (
} ) ;
value . price ? . regularMarketPrice ? ? 0
)
. div ( 100 )
. toNumber ( ) ;
}
// Add country if stock and available
if ( code ) {
if (
response . countries = [ { code , weight : 1 } ] ;
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 }
] ;
}
}
}
} catch { }
// Add url if available
if ( assetProfile . summaryProfile ? . sector ) {
const url = value . summaryProfile ? . website ;
response . sectors = [
if ( url ) {
{ name : assetProfile.summaryProfile?.sector , weight : 1 }
response [ symbol ] . url = url ;
] ;
}
}
}
}
return response ;
const url = assetProfile . summaryProfile ? . website ;
} catch ( error ) {
if ( url ) {
Logger . error ( error ) ;
response . url = url ;
}
} catch { }
return { } ;
return response ;
}
}
}
public async getHistorical (
public async getHistorical (
aSymbol s : string [ ] ,
aSymbol : string ,
aGranularity : Granularity = 'day' ,
aGranularity : Granularity = 'day' ,
from : Date ,
from : Date ,
to : Date
to : Date
) : Promise < {
) : Promise < {
[ symbol : string ] : { [ date : string ] : IDataProviderHistoricalResponse } ;
[ symbol : string ] : { [ date : string ] : IDataProviderHistoricalResponse } ;
} > {
} > {
if ( aSymbols . length <= 0 ) {
return { } ;
}
if ( isSameDay ( from , to ) ) {
if ( isSameDay ( from , to ) ) {
to = addDays ( to , 1 ) ;
to = addDays ( to , 1 ) ;
}
}
const yahooFinanceSymbols = aSymbols . map ( ( symbol ) = > {
const yahooFinanceSymbol = this . convertToYahooFinanceSymbol ( aSymbol ) ;
return this . convertToYahooFinanceSymbol ( symbol ) ;
} ) ;
try {
try {
const historicalData : {
const historicalResult = await yahooFinance2 . historical (
[ symbol : string ] : IYahooFinanceHistoricalResponse [ ] ;
yahooFinanceSymbol ,
} = await yahooFinance . historical ( {
{
symbols : yahooFinanceSymbols ,
interval : '1d' ,
from : format ( from , DATE_FORMAT ) ,
period1 : format ( from , DATE_FORMAT ) ,
to : format ( to , DATE_FORMAT )
period2 : format ( to , DATE_FORMAT )
} ) ;
}
) ;
const response : {
const response : {
[ symbol : string ] : { [ date : string ] : IDataProviderHistoricalResponse } ;
[ symbol : string ] : { [ date : string ] : IDataProviderHistoricalResponse } ;
} = { } ;
} = { } ;
for ( const [ yahooFinanceSymbol , timeSeries ] of Object . entries (
// Convert symbol back
historicalData
const symbol = this . convertFromYahooFinanceSymbol ( yahooFinanceSymbol ) ;
) ) {
// Convert symbols back
const symbol = this . convertFromYahooFinanceSymbol ( yahooFinanceSymbol ) ;
response [ symbol ] = { } ;
timeSeries . forEach ( ( timeSerie ) = > {
response [ symbol ] = { } ;
response [ symbol ] [ format ( timeSerie . date , DATE_FORMAT ) ] = {
marketPrice : timeSerie.close ,
for ( const historicalItem of historicalResult ) {
performance : timeSerie.open - timeSerie . close
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 ;
return response ;
} catch ( error ) {
} catch ( error ) {
Logger . error ( error ) ;
Logger . warn (
` Skipping yahooFinance2.getHistorical(" ${ aSymbol } "): [ ${ error . name } ] ${ error . message } `
) ;
return { } ;
return { } ;
}
}
@ -221,6 +189,56 @@ export class YahooFinanceService implements DataProviderInterface {
return DataSource . YAHOO ;
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 [ ] } > {
public async search ( aQuery : string ) : Promise < { items : LookupItem [ ] } > {
const items : LookupItem [ ] = [ ] ;
const items : LookupItem [ ] = [ ] ;
@ -236,7 +254,7 @@ export class YahooFinanceService implements DataProviderInterface {
const searchResult = await get ( ) ;
const searchResult = await get ( ) ;
const symbols: string [ ] = searchResult . quotes
const quotes = searchResult . quotes
. filter ( ( quote ) = > {
. filter ( ( quote ) = > {
// filter out undefined symbols
// filter out undefined symbols
return quote . symbol ;
return quote . symbol ;
@ -247,8 +265,7 @@ export class YahooFinanceService implements DataProviderInterface {
this . cryptocurrencyService . isCryptocurrency (
this . cryptocurrencyService . isCryptocurrency (
symbol . replace ( new RegExp ( ` - ${ baseCurrency } $ ` ) , baseCurrency )
symbol . replace ( new RegExp ( ` - ${ baseCurrency } $ ` ) , baseCurrency )
) ) ||
) ) ||
quoteType === 'EQUITY' ||
[ 'EQUITY' , 'ETF' , 'MUTUALFUND' ] . includes ( quoteType )
quoteType === 'ETF'
) ;
) ;
} )
} )
. filter ( ( { quoteType , symbol } ) = > {
. filter ( ( { quoteType , symbol } ) = > {
@ -259,19 +276,24 @@ export class YahooFinanceService implements DataProviderInterface {
}
}
return true ;
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 ) ) {
for ( const [ symbol , value ] of Object . entries ( marketData ) ) {
const quote = quotes . find ( ( currentQuote : any ) = > {
return currentQuote . symbol === symbol ;
} ) ;
items . push ( {
items . push ( {
symbol ,
symbol ,
currency : value.currency ,
currency : value.currency ,
dataSource : this.getName ( ) ,
dataSource : this.getName ( ) ,
name : value.name
name : quote?.longname || quote ? . shortname || symbol
} ) ;
} ) ;
}
}
} catch ( error ) {
} catch ( error ) {
@ -281,7 +303,7 @@ export class YahooFinanceService implements DataProviderInterface {
return { items } ;
return { items } ;
}
}
private parseAssetClass ( aPrice : IYahooFinancePrice ) : {
private parseAssetClass ( aPrice : any ) : {
assetClass : AssetClass ;
assetClass : AssetClass ;
assetSubClass : AssetSubClass ;
assetSubClass : AssetSubClass ;
} {
} {
@ -301,16 +323,12 @@ export class YahooFinanceService implements DataProviderInterface {
assetClass = AssetClass . EQUITY ;
assetClass = AssetClass . EQUITY ;
assetSubClass = AssetSubClass . ETF ;
assetSubClass = AssetSubClass . ETF ;
break ;
break ;
case 'mutualfund' :
assetClass = AssetClass . EQUITY ;
assetSubClass = AssetSubClass . MUTUALFUND ;
break ;
}
}
return { assetClass , assetSubClass } ;
return { assetClass , assetSubClass } ;
}
}
private parseExchange ( aString : string ) : string {
if ( aString ? . toLowerCase ( ) === 'ccc' ) {
return UNKNOWN_KEY ;
}
return aString ;
}
}
}