Allow custom currency in activity import (#2215)
* Allow custom currency in activity import * Extend import test files * Update changelog --------- Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
parent
de93cabd69
commit
2d003225bc
@ -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
|
||||
|
@ -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}"`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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,<invalid>,44558.42,1,buy,0
|
||||
|
|
19
test/import/unavailable-exchange-rate.json
Normal file
19
test/import/unavailable-exchange-rate.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user