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/),
|
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).
|
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
|
## 1.298.0 - 2023-08-06
|
||||||
|
|
||||||
### Changed
|
### 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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
|
DATE_FORMAT,
|
||||||
getAssetProfileIdentifier,
|
getAssetProfileIdentifier,
|
||||||
parseDate
|
parseDate
|
||||||
} from '@ghostfolio/common/helper';
|
} from '@ghostfolio/common/helper';
|
||||||
@ -24,7 +25,7 @@ import {
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
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 { uniqBy } from 'lodash';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
@ -248,17 +249,20 @@ export class ImportService {
|
|||||||
|
|
||||||
const activities: Activity[] = [];
|
const activities: Activity[] = [];
|
||||||
|
|
||||||
for (const {
|
for (let [
|
||||||
accountId,
|
index,
|
||||||
comment,
|
{
|
||||||
date,
|
accountId,
|
||||||
error,
|
comment,
|
||||||
fee,
|
date,
|
||||||
quantity,
|
error,
|
||||||
SymbolProfile,
|
fee,
|
||||||
type,
|
quantity,
|
||||||
unitPrice
|
SymbolProfile,
|
||||||
} of activitiesExtendedWithErrors) {
|
type,
|
||||||
|
unitPrice
|
||||||
|
}
|
||||||
|
] of activitiesExtendedWithErrors.entries()) {
|
||||||
const assetProfile = assetProfiles[
|
const assetProfile = assetProfiles[
|
||||||
getAssetProfileIdentifier({
|
getAssetProfileIdentifier({
|
||||||
dataSource: SymbolProfile.dataSource,
|
dataSource: SymbolProfile.dataSource,
|
||||||
@ -296,6 +300,35 @@ export class ImportService {
|
|||||||
Account?: { id: string; name: string };
|
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) {
|
if (isDryRun) {
|
||||||
order = {
|
order = {
|
||||||
comment,
|
comment,
|
||||||
@ -533,15 +566,21 @@ export class ImportService {
|
|||||||
])
|
])
|
||||||
)?.[symbol];
|
)?.[symbol];
|
||||||
|
|
||||||
if (assetProfile === undefined) {
|
if (!assetProfile) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`activities.${index}.symbol ("${symbol}") is not valid for the specified data source ("${dataSource}")`
|
`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(
|
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;
|
return this.currencyPairs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public hasCurrencyPair(currency1: string, currency2: string) {
|
||||||
|
return this.currencyPairs.some(({ symbol }) => {
|
||||||
|
return (
|
||||||
|
symbol === `${currency1}${currency2}` ||
|
||||||
|
symbol === `${currency2}${currency1}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public async initialize() {
|
public async initialize() {
|
||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||||
this.currencies = await this.prepareCurrencies();
|
this.currencies = await this.prepareCurrencies();
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
Date,Code,Currency,Price,Quantity,Action,Fee
|
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