diff --git a/CHANGELOG.md b/CHANGELOG.md index 42cad54a0..2a75a6a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 + +### Changed + +- Optimized the activities import by allowing a different currency than the asset's official one + ## 1.298.0 - 2023-08-06 ### Changed diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 3d9c2999d..026f3610e 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -13,6 +13,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { + DATE_FORMAT, getAssetProfileIdentifier, parseDate } from '@ghostfolio/common/helper'; @@ -24,7 +25,7 @@ import { import { Injectable } from '@nestjs/common'; import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import Big from 'big.js'; -import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns'; +import { endOfToday, format, isAfter, isSameDay, parseISO } from 'date-fns'; import { uniqBy } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; @@ -248,17 +249,20 @@ export class ImportService { const activities: Activity[] = []; - for (const { - accountId, - comment, - date, - error, - fee, - quantity, - SymbolProfile, - type, - unitPrice - } of activitiesExtendedWithErrors) { + for (let [ + index, + { + accountId, + comment, + date, + error, + fee, + quantity, + SymbolProfile, + type, + unitPrice + } + ] of activitiesExtendedWithErrors.entries()) { const assetProfile = assetProfiles[ getAssetProfileIdentifier({ dataSource: SymbolProfile.dataSource, @@ -296,6 +300,35 @@ export class ImportService { Account?: { id: string; name: string }; }); + if (SymbolProfile.currency !== assetProfile.currency) { + // Convert the unit price and fee to the asset currency if the imported + // activity is in a different currency + unitPrice = await this.exchangeRateDataService.toCurrencyAtDate( + unitPrice, + SymbolProfile.currency, + assetProfile.currency, + date + ); + + if (!unitPrice) { + throw new Error( + `activities.${index} historical exchange rate at ${format( + date, + DATE_FORMAT + )} is not available from "${SymbolProfile.currency}" to "${ + assetProfile.currency + }"` + ); + } + + fee = await this.exchangeRateDataService.toCurrencyAtDate( + fee, + SymbolProfile.currency, + assetProfile.currency, + date + ); + } + if (isDryRun) { order = { comment, @@ -533,15 +566,21 @@ export class ImportService { ]) )?.[symbol]; - if (assetProfile === undefined) { + if (!assetProfile) { throw new Error( `activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")` ); } - if (assetProfile.currency !== currency) { + if ( + assetProfile.currency !== currency && + !this.exchangeRateDataService.hasCurrencyPair( + currency, + assetProfile.currency + ) + ) { throw new Error( - `activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}"` + `activities.${index}.currency ("${currency}") does not match with "${assetProfile.currency}" and no exchange rate is available from "${currency}" to "${assetProfile.currency}"` ); } diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index d94037530..24b0ed5d0 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -33,6 +33,15 @@ export class ExchangeRateDataService { return this.currencyPairs; } + public hasCurrencyPair(currency1: string, currency2: string) { + return this.currencyPairs.some(({ symbol }) => { + return ( + symbol === `${currency1}${currency2}` || + symbol === `${currency2}${currency1}` + ); + }); + } + public async initialize() { this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); this.currencies = await this.prepareCurrencies(); diff --git a/test/import/invalid-currency.csv b/test/import/invalid-currency.csv index e04db317b..6782047c7 100644 --- a/test/import/invalid-currency.csv +++ b/test/import/invalid-currency.csv @@ -1,2 +1,2 @@ Date,Code,Currency,Price,Quantity,Action,Fee -12/12/2021,BTC,EUR,44558.42,1,buy,0 +12/12/2021,BTC,,44558.42,1,buy,0 diff --git a/test/import/unavailable-exchange-rate.json b/test/import/unavailable-exchange-rate.json new file mode 100644 index 000000000..2d21a76c3 --- /dev/null +++ b/test/import/unavailable-exchange-rate.json @@ -0,0 +1,19 @@ +{ + "meta": { + "date": "2023-02-05T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "comment": null, + "fee": 0, + "quantity": 0, + "type": "BUY", + "unitPrice": 0, + "currency": "EUR", + "dataSource": "YAHOO", + "date": "1990-01-01T22:00:00.000Z", + "symbol": "MSFT" + } + ] +}