diff --git a/apps/api/src/assets/stocks/market-currencies.json b/apps/api/src/assets/stocks/market-currencies.json new file mode 100644 index 000000000..8d6628641 --- /dev/null +++ b/apps/api/src/assets/stocks/market-currencies.json @@ -0,0 +1,65 @@ +{ + "VN": "VND", + "BK": "THB", + "T": "JPY", + "KQ": "KRW", + "KS": "KRW", + "SS": "CNY", + "SZ": "CNY", + "HK": "HKD", + "L": "GBP", + "IL": "GBP", + "AX": "AUD", + "TO": "CAD", + "V": "CAD", + "NE": "CAD", + "CN": "CAD", + "PA": "EUR", + "NX": "EUR", + "BR": "EUR", + "AS": "EUR", + "IR": "EUR", + "LS": "EUR", + "MC": "EUR", + "SN": "CLP", + "SA": "BRL", + "MX": "MXN", + "BO": "INR", + "NS": "INR", + "JK": "IDR", + "KL": "MYR", + "SI": "SGD", + "TW": "TWD", + "TWO": "TWD", + "CO": "DKK", + "HE": "EUR", + "ST": "SEK", + "SW": "CHF", + "F": "EUR", + "BE": "EUR", + "BM": "EUR", + "DU": "EUR", + "HM": "EUR", + "HA": "EUR", + "MU": "EUR", + "SG": "EUR", + "DE": "EUR", + "AT": "EUR", + "CA": "EGP", + "TA": "ILS", + "MI": "EUR", + "TI": "EUR", + "PR": "CZK", + "BD": "HUF", + "IC": "ISK", + "TL": "EUR", + "RG": "EUR", + "VS": "EUR", + "OL": "NOK", + "QA": "QAR", + "ME": "RUB", + "IS": "TRY", + "SAU": "SAR", + "CR": "VES", + "NZ": "NZD" +} diff --git a/apps/api/src/helper/market-currencies.helper.spec.ts b/apps/api/src/helper/market-currencies.helper.spec.ts new file mode 100644 index 000000000..7f95f96d0 --- /dev/null +++ b/apps/api/src/helper/market-currencies.helper.spec.ts @@ -0,0 +1,37 @@ +import { lookupCurrency } from '@ghostfolio/api/helper/market-currencies.helper'; + +describe('lookupCurrency', () => { + it('should return null if param is empty or null', () => { + expect(lookupCurrency(null)).toBeNull(); + + expect(lookupCurrency('')).toBeNull(); + + expect(lookupCurrency(' ')).toBeNull(); + }); + + it('should return corresponding market currency from suffix code', () => { + expect(lookupCurrency('VN')).toStrictEqual('VND'); + + expect(lookupCurrency('T')).toStrictEqual('JPY'); + }); +}); + +describe('determineStockCurrency', () => { + it('should return corresponding market currency from symbol has suffix code', () => { + expect(lookupCurrency('MBB.VN')).toStrictEqual('VND'); + + expect(lookupCurrency('7203.T')).toStrictEqual('JPY'); + + expect(lookupCurrency('EA.BK')).toStrictEqual('THB'); + }); + + it('should return null if symbol has no suffix code', () => { + expect(lookupCurrency('APPL')).toBeNull(); + + expect(lookupCurrency('TLSA')).toBeNull(); + }); + + it('should return null if symbol has suffix code but not supported in json data', () => { + expect(lookupCurrency('A.CBT')).toBeNull(); + }); +}); diff --git a/apps/api/src/helper/market-currencies.helper.ts b/apps/api/src/helper/market-currencies.helper.ts new file mode 100644 index 000000000..dcb25af97 --- /dev/null +++ b/apps/api/src/helper/market-currencies.helper.ts @@ -0,0 +1,37 @@ +const stockExchangeCurrencies: { + [key: string]: string; +} = require('../assets/stocks/market-currencies.json'); + +/** + * + * @param marketSuffixCode - The market suffix code (e.g. 'BK', 'VN', 'T' etc.) + * @description This function retrieves the currency associated with a given market suffix code from Yahoo Finance. + * It uses a "stock-exchange-currencies.json" file that contains the mapping of market suffix codes to their respective currencies. + * If there is no matching currency for the provided market suffix code, the function returns null. + * Please refer: https://help.yahoo.com/kb/SLN2310.html + * + * @returns string | null - The currency associated with the market suffix code, or null if not found. + */ +export function lookupCurrency(marketSuffixCode: string): string | null { + if (marketSuffixCode in stockExchangeCurrencies) { + return stockExchangeCurrencies[marketSuffixCode]; + } + return null; +} + +/** + * + * @param symbol - The symbol to determine the market currency for (e.g. 'MBB.VN', 'EA.BK', etc.) + * @description This function extracts the market suffix code from the provided symbol and retrieves the corresponding currency. + * It uses the "getMarketCurrency" function to perform the lookup. + * + * @returns string | null - The currency associated with the market suffix code, or null if not found or the symbol do not have market suffix code. + */ +export function determineStockCurrency(symbol: string): string | null { + const marketSuffixCode = symbol.split('.').pop(); + if (!marketSuffixCode) { + return null; + } + + return lookupCurrency(marketSuffixCode); +} diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 94a466742..eb596788c 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -1,3 +1,4 @@ +import { determineStockCurrency } from '@ghostfolio/api/helper/market-currencies.helper'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; @@ -190,7 +191,8 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { response.assetClass = assetClass; response.assetSubClass = assetSubClass; - response.currency = assetProfile.price.currency; + response.currency = + assetProfile.price.currency ?? determineStockCurrency(symbol); response.dataSource = this.getName(); response.name = this.formatName({ longName: assetProfile.price.longName, 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 d5a132b41..ee07bec7a 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,3 +1,4 @@ +import { determineStockCurrency } from '@ghostfolio/api/helper/market-currencies.helper'; 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'; @@ -25,6 +26,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import { addDays, format, isSameDay } from 'date-fns'; import YahooFinance from 'yahoo-finance2'; +import { ModuleOptionsWithValidateTrue } from 'yahoo-finance2/esm/src/lib/moduleCommon'; import { ChartResultArray } from 'yahoo-finance2/esm/src/modules/chart'; import { HistoricalDividendsResult, @@ -81,8 +83,12 @@ export class YahooFinanceService implements DataProviderInterface { events: 'dividends', interval: granularity === 'month' ? '1mo' : '1d', period1: format(from, DATE_FORMAT), - period2: format(to, DATE_FORMAT) - } + period2: format(to, DATE_FORMAT), + return: 'array' + }, + { + validateResult: determineStockCurrency(symbol) == null + } as ModuleOptionsWithValidateTrue ) ); const response: { @@ -129,8 +135,12 @@ export class YahooFinanceService implements DataProviderInterface { { interval: '1d', period1: format(from, DATE_FORMAT), - period2: format(to, DATE_FORMAT) - } + period2: format(to, DATE_FORMAT), + return: 'array' + }, + { + validateResult: determineStockCurrency(symbol) == null + } as ModuleOptionsWithValidateTrue ) ); @@ -211,7 +221,7 @@ export class YahooFinanceService implements DataProviderInterface { ); response[symbol] = { - currency: quote.currency, + currency: quote.currency ?? determineStockCurrency(symbol), dataSource: this.getName(), marketState: quote.marketState === 'REGULAR' || @@ -307,7 +317,7 @@ export class YahooFinanceService implements DataProviderInterface { assetClass, assetSubClass, symbol, - currency: marketDataItem.currency, + currency: marketDataItem.currency ?? determineStockCurrency(symbol), dataProviderInfo: this.getDataProviderInfo(), dataSource: this.getName(), name: this.yahooFinanceDataEnhancerService.formatName({