Compare commits
1 Commits
main
...
feature/da
Author | SHA1 | Date | |
---|---|---|---|
|
a91f6c783e |
@ -2,8 +2,10 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import { Big } from 'big.js';
|
||||
|
||||
import { CurrentRateService } from './current-rate.service';
|
||||
import { DateQuery } from './interfaces/date-query.interface';
|
||||
|
||||
jest.mock('@ghostfolio/api/services/market-data.service', () => {
|
||||
return {
|
||||
@ -57,6 +59,26 @@ jest.mock('@ghostfolio/api/services/exchange-rate-data.service', () => {
|
||||
ExchangeRateDataService: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
initialize: () => Promise.resolve(),
|
||||
getExchangeRates: ({
|
||||
dateQuery,
|
||||
sourceCurrencies,
|
||||
destinationCurrency
|
||||
}: {
|
||||
dateQuery: DateQuery;
|
||||
sourceCurrencies: string[];
|
||||
destinationCurrency: string;
|
||||
}) => {
|
||||
return [
|
||||
{
|
||||
date: new Date(),
|
||||
exchangeRates: {
|
||||
USD: new Big(1),
|
||||
CHF: new Big(1),
|
||||
EUR: new Big(1)
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
toCurrency: (value: number) => {
|
||||
return 1 * value;
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isBefore, isToday } from 'date-fns';
|
||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Big } from 'big.js';
|
||||
import { format, isAfter, isBefore, isToday } from 'date-fns';
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
import { GetValueObject } from './interfaces/get-value-object.interface';
|
||||
@ -77,6 +78,13 @@ export class CurrentRateService {
|
||||
}[]
|
||||
>[] = [];
|
||||
|
||||
const sourceCurrencies = Object.values(currencies);
|
||||
const exchangeRates = await this.exchangeRateDataService.getExchangeRates({
|
||||
dateQuery,
|
||||
sourceCurrencies,
|
||||
destinationCurrency: userCurrency
|
||||
});
|
||||
|
||||
if (includeToday) {
|
||||
const today = resetHours(new Date());
|
||||
promises.push(
|
||||
@ -112,17 +120,59 @@ export class CurrentRateService {
|
||||
symbols
|
||||
})
|
||||
.then((data) => {
|
||||
return data.map((marketDataItem) => {
|
||||
return {
|
||||
date: marketDataItem.date,
|
||||
marketPrice: this.exchangeRateDataService.toCurrency(
|
||||
const result = [];
|
||||
let j = 0;
|
||||
for (const marketDataItem of data) {
|
||||
const currency = currencies[marketDataItem.symbol];
|
||||
while (
|
||||
j + 1 < exchangeRates.length &&
|
||||
!isAfter(exchangeRates[j + 1].date, marketDataItem.date)
|
||||
) {
|
||||
j++;
|
||||
}
|
||||
let exchangeRate: Big;
|
||||
if (currency !== userCurrency) {
|
||||
exchangeRate = exchangeRates[j]?.exchangeRates[currency];
|
||||
|
||||
for (
|
||||
let k = j;
|
||||
k >= 0 && !exchangeRates[k]?.exchangeRates[currency];
|
||||
k--
|
||||
) {
|
||||
exchangeRate = exchangeRates[k]?.exchangeRates[currency];
|
||||
}
|
||||
} else {
|
||||
exchangeRate = new Big(1);
|
||||
}
|
||||
let marketPrice: number;
|
||||
if (exchangeRate) {
|
||||
marketPrice = exchangeRate
|
||||
.mul(marketDataItem.marketPrice)
|
||||
.toNumber();
|
||||
} else {
|
||||
if (!isToday(marketDataItem.date)) {
|
||||
Logger.error(
|
||||
`Failed to get exchange rate for ${
|
||||
currencies[marketDataItem.symbol]
|
||||
} to ${userCurrency} at ${format(
|
||||
marketDataItem.date,
|
||||
DATE_FORMAT
|
||||
)}, using today's exchange rate as a fallback`
|
||||
);
|
||||
}
|
||||
marketPrice = this.exchangeRateDataService.toCurrency(
|
||||
marketDataItem.marketPrice,
|
||||
currencies[marketDataItem.symbol],
|
||||
userCurrency
|
||||
),
|
||||
);
|
||||
}
|
||||
result.push({
|
||||
date: marketDataItem.date,
|
||||
marketPrice: marketPrice,
|
||||
symbol: marketDataItem.symbol
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
baseCurrency,
|
||||
ghostfolioCashSymbol
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
PortfolioDetails,
|
||||
@ -43,7 +43,7 @@ import type {
|
||||
OrderWithAccount,
|
||||
RequestWithUser
|
||||
} from '@ghostfolio/common/types';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
@ -52,6 +52,7 @@ import {
|
||||
format,
|
||||
isAfter,
|
||||
isBefore,
|
||||
isToday,
|
||||
max,
|
||||
parse,
|
||||
parseISO,
|
||||
@ -735,20 +736,24 @@ export class PortfolioService {
|
||||
};
|
||||
}
|
||||
|
||||
public getFees(orders: OrderWithAccount[], date = new Date(0)) {
|
||||
public getFees(
|
||||
orders: OrderWithAccount[],
|
||||
exchangeRates: { [date: string]: { [currency: string]: Big } },
|
||||
date = new Date(0)
|
||||
) {
|
||||
return orders
|
||||
.filter((order) => {
|
||||
// Filter out all orders before given date
|
||||
return isBefore(date, new Date(order.date));
|
||||
})
|
||||
.map((order) => {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.currency,
|
||||
this.request.user.Settings.currency
|
||||
);
|
||||
})
|
||||
.reduce((previous, current) => previous + current, 0);
|
||||
.map((order) =>
|
||||
this.convertCurrency({
|
||||
exchangeRates,
|
||||
...order,
|
||||
value: order.fee
|
||||
})
|
||||
)
|
||||
.reduce((previous, current) => current.plus(previous), new Big(0));
|
||||
}
|
||||
|
||||
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||
@ -786,6 +791,7 @@ export class PortfolioService {
|
||||
currency,
|
||||
userId
|
||||
);
|
||||
const exchangeRates = await this.exchangeRateForOrders(orders);
|
||||
return {
|
||||
rules: {
|
||||
accountClusterRisk: await this.rulesService.evaluate(
|
||||
@ -831,7 +837,7 @@ export class PortfolioService {
|
||||
new FeeRatioInitialInvestment(
|
||||
this.exchangeRateDataService,
|
||||
currentPositions.totalInvestment.toNumber(),
|
||||
this.getFees(orders)
|
||||
this.getFees(orders, exchangeRates).toNumber()
|
||||
)
|
||||
],
|
||||
{ baseCurrency: currency }
|
||||
@ -851,11 +857,20 @@ export class PortfolioService {
|
||||
currency
|
||||
);
|
||||
const orders = await this.orderService.getOrders({ userId });
|
||||
const fees = this.getFees(orders);
|
||||
const exchangeRates = await this.exchangeRateForOrders(orders);
|
||||
const fees = this.getFees(orders, exchangeRates);
|
||||
const firstOrderDate = orders[0]?.date;
|
||||
|
||||
const totalBuy = this.getTotalByType(orders, currency, TypeOfOrder.BUY);
|
||||
const totalSell = this.getTotalByType(orders, currency, TypeOfOrder.SELL);
|
||||
const totalBuy = this.getTotalByType(
|
||||
orders,
|
||||
TypeOfOrder.BUY,
|
||||
exchangeRates
|
||||
);
|
||||
const totalSell = this.getTotalByType(
|
||||
orders,
|
||||
TypeOfOrder.SELL,
|
||||
exchangeRates
|
||||
);
|
||||
|
||||
const committedFunds = new Big(totalBuy).sub(totalSell);
|
||||
|
||||
@ -865,14 +880,14 @@ export class PortfolioService {
|
||||
|
||||
return {
|
||||
...performanceInformation.performance,
|
||||
fees,
|
||||
firstOrderDate,
|
||||
netWorth,
|
||||
cash: balance,
|
||||
committedFunds: committedFunds.toNumber(),
|
||||
fees: fees.toNumber(),
|
||||
ordersCount: orders.length,
|
||||
totalBuy: totalBuy,
|
||||
totalSell: totalSell
|
||||
totalBuy: totalBuy.toNumber(),
|
||||
totalSell: totalSell.toNumber()
|
||||
};
|
||||
}
|
||||
|
||||
@ -980,28 +995,25 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||
const exchangeRates = await this.exchangeRateForOrders(orders);
|
||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||
currency: order.currency,
|
||||
dataSource: order.dataSource,
|
||||
date: format(order.date, DATE_FORMAT),
|
||||
fee: new Big(
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
order.fee,
|
||||
order.currency,
|
||||
userCurrency
|
||||
)
|
||||
),
|
||||
fee: this.convertCurrency({
|
||||
exchangeRates,
|
||||
...order,
|
||||
value: order.fee
|
||||
}),
|
||||
name: order.SymbolProfile?.name,
|
||||
quantity: new Big(order.quantity),
|
||||
symbol: order.symbol,
|
||||
type: <OrderType>order.type,
|
||||
unitPrice: new Big(
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
order.unitPrice,
|
||||
order.currency,
|
||||
userCurrency
|
||||
)
|
||||
)
|
||||
unitPrice: this.convertCurrency({
|
||||
exchangeRates,
|
||||
...order,
|
||||
value: order.unitPrice
|
||||
})
|
||||
}));
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator(
|
||||
@ -1071,6 +1083,60 @@ export class PortfolioService {
|
||||
return accounts;
|
||||
}
|
||||
|
||||
private convertCurrency({
|
||||
exchangeRates,
|
||||
date,
|
||||
currency,
|
||||
value
|
||||
}: {
|
||||
exchangeRates: { [date: string]: { [currency: string]: Big } };
|
||||
date: Date;
|
||||
currency: string;
|
||||
value: number | Big;
|
||||
}): Big {
|
||||
const exchangeRate = exchangeRates[format(date, DATE_FORMAT)]?.[currency];
|
||||
if (exchangeRate) {
|
||||
return exchangeRate?.mul(value);
|
||||
}
|
||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||
if (!isToday(date)) {
|
||||
Logger.error(
|
||||
`Failed to convert value for date ${format(
|
||||
date,
|
||||
DATE_FORMAT
|
||||
)} from ${currency} to ${userCurrency}`
|
||||
);
|
||||
}
|
||||
return new Big(
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
new Big(value).toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async exchangeRateForOrders(
|
||||
orders: OrderWithAccount[]
|
||||
): Promise<{ [date: string]: { [currency: string]: Big } }> {
|
||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||
|
||||
const dates = orders.map((order) => resetHours(order.date));
|
||||
const exchangeRates = await this.exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
in: dates
|
||||
},
|
||||
sourceCurrencies: orders.map((order) => order.currency),
|
||||
destinationCurrency: userCurrency
|
||||
});
|
||||
const exchangeRateLookupMap = {};
|
||||
for (const exchangeRate of exchangeRates) {
|
||||
exchangeRateLookupMap[format(exchangeRate.date, DATE_FORMAT)] =
|
||||
exchangeRate.exchangeRates;
|
||||
}
|
||||
return exchangeRateLookupMap;
|
||||
}
|
||||
|
||||
private async getUserId(aImpersonationId: string, aUserId: string) {
|
||||
const impersonationUserId =
|
||||
await this.impersonationService.validateImpersonationId(
|
||||
@ -1083,20 +1149,20 @@ export class PortfolioService {
|
||||
|
||||
private getTotalByType(
|
||||
orders: OrderWithAccount[],
|
||||
currency: string,
|
||||
type: TypeOfOrder
|
||||
type: TypeOfOrder,
|
||||
exchangeRates: { [date: string]: { [currency: string]: Big } }
|
||||
) {
|
||||
return orders
|
||||
.filter(
|
||||
(order) => !isAfter(order.date, endOfToday()) && order.type === type
|
||||
)
|
||||
.map((order) => {
|
||||
return this.exchangeRateDataService.toCurrency(
|
||||
order.quantity * order.unitPrice,
|
||||
order.currency,
|
||||
currency
|
||||
);
|
||||
})
|
||||
.reduce((previous, current) => previous + current, 0);
|
||||
.map((order) =>
|
||||
this.convertCurrency({
|
||||
exchangeRates,
|
||||
...order,
|
||||
value: order.quantity * order.unitPrice
|
||||
})
|
||||
)
|
||||
.reduce((previous, current) => current.plus(previous), new Big(0));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PrismaModule } from './prisma.module';
|
||||
@ -7,7 +8,7 @@ import { PropertyModule } from './property/property.module';
|
||||
|
||||
@Module({
|
||||
imports: [DataProviderModule, PrismaModule, PropertyModule],
|
||||
providers: [ExchangeRateDataService],
|
||||
providers: [ExchangeRateDataService, MarketDataService],
|
||||
exports: [ExchangeRateDataService]
|
||||
})
|
||||
export class ExchangeRateDataModule {}
|
||||
|
201
apps/api/src/services/exchange-rate-data.service.spec.ts
Normal file
201
apps/api/src/services/exchange-rate-data.service.spec.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { MarketData } from '@prisma/client';
|
||||
import { Big } from 'big.js';
|
||||
import { addDays, endOfDay, isBefore } from 'date-fns';
|
||||
|
||||
jest.mock('@ghostfolio/api/services/market-data.service', () => {
|
||||
return {
|
||||
MarketDataService: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getRange: ({
|
||||
dateQuery,
|
||||
symbols
|
||||
}: {
|
||||
dateQuery: DateQuery;
|
||||
symbols: string[];
|
||||
}) => {
|
||||
const exchangeRateMap = {
|
||||
USDEUR: 1,
|
||||
USDCHF: 2,
|
||||
USDUSD: 0
|
||||
};
|
||||
const result = [];
|
||||
let j = 1;
|
||||
for (
|
||||
let i = dateQuery.gte;
|
||||
isBefore(i, dateQuery.lt);
|
||||
i = addDays(i, 1)
|
||||
) {
|
||||
const marketPrice = j++;
|
||||
for (const symbol of symbols) {
|
||||
result.push({
|
||||
createdAt: i,
|
||||
date: i,
|
||||
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
|
||||
marketPrice: marketPrice * exchangeRateMap[symbol] + 1,
|
||||
symbol: symbol
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve<MarketData[]>(result);
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('ExchangeRateDataService', () => {
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let marketDataService: MarketDataService;
|
||||
|
||||
beforeAll(async () => {
|
||||
marketDataService = new MarketDataService(null);
|
||||
exchangeRateDataService = new ExchangeRateDataService(
|
||||
null,
|
||||
marketDataService,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
describe('getExchangeRates', () => {
|
||||
it('source and destination USD', async () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const exchangeRates = await exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(startDate)
|
||||
},
|
||||
sourceCurrencies: ['USD'],
|
||||
destinationCurrency: 'USD'
|
||||
});
|
||||
|
||||
expect(exchangeRates).toEqual([
|
||||
{
|
||||
date: startDate,
|
||||
exchangeRates: {
|
||||
USD: new Big(1)
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('source USD and destination CHF', async () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const exchangeRates = await exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(startDate)
|
||||
},
|
||||
sourceCurrencies: ['USD'],
|
||||
destinationCurrency: 'CHF'
|
||||
});
|
||||
|
||||
expect(exchangeRates).toEqual([
|
||||
{
|
||||
date: startDate,
|
||||
exchangeRates: {
|
||||
USD: new Big(3)
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('source CHF and destination USD', async () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const exchangeRates = await exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(startDate)
|
||||
},
|
||||
sourceCurrencies: ['CHF'],
|
||||
destinationCurrency: 'USD'
|
||||
});
|
||||
|
||||
expect(exchangeRates).toEqual([
|
||||
{
|
||||
date: startDate,
|
||||
exchangeRates: {
|
||||
CHF: new Big(1).div(3)
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('source CHF and destination EUR', async () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const exchangeRates = await exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(startDate)
|
||||
},
|
||||
sourceCurrencies: ['CHF'],
|
||||
destinationCurrency: 'EUR'
|
||||
});
|
||||
|
||||
expect(exchangeRates).toEqual([
|
||||
{
|
||||
date: startDate,
|
||||
exchangeRates: {
|
||||
CHF: new Big(2).div(3)
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('source CHF,EUR,USD and destination EUR', async () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const exchangeRates = await exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(startDate)
|
||||
},
|
||||
sourceCurrencies: ['CHF', 'USD', 'EUR'],
|
||||
destinationCurrency: 'EUR'
|
||||
});
|
||||
|
||||
expect(exchangeRates).toEqual([
|
||||
{
|
||||
date: startDate,
|
||||
exchangeRates: {
|
||||
CHF: new Big(2).div(3),
|
||||
USD: new Big(2),
|
||||
EUR: new Big(1)
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('with multiple days', async () => {
|
||||
const startDate = new Date(2021, 0, 1);
|
||||
const exchangeRates = await exchangeRateDataService.getExchangeRates({
|
||||
dateQuery: {
|
||||
gte: startDate,
|
||||
lt: endOfDay(addDays(startDate, 1))
|
||||
},
|
||||
sourceCurrencies: ['CHF', 'USD', 'EUR'],
|
||||
destinationCurrency: 'EUR'
|
||||
});
|
||||
|
||||
expect(exchangeRates).toEqual([
|
||||
{
|
||||
date: startDate,
|
||||
exchangeRates: {
|
||||
CHF: new Big(2).div(3),
|
||||
USD: new Big(2),
|
||||
EUR: new Big(1)
|
||||
}
|
||||
},
|
||||
{
|
||||
date: addDays(startDate, 1),
|
||||
exchangeRates: {
|
||||
CHF: new Big(3).div(5),
|
||||
USD: new Big(3),
|
||||
EUR: new Big(1)
|
||||
}
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,7 +1,11 @@
|
||||
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
||||
import { DateBasedExchangeRate } from '@ghostfolio/api/services/interfaces/date-based-exchange-rate.interface';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { format } from 'date-fns';
|
||||
import Big from 'big.js';
|
||||
import { format, isSameDay } from 'date-fns';
|
||||
import { isEmpty, isNumber, uniq } from 'lodash';
|
||||
|
||||
import { DataProviderService } from './data-provider/data-provider.service';
|
||||
@ -17,6 +21,7 @@ export class ExchangeRateDataService {
|
||||
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly prismaService: PrismaService,
|
||||
private readonly propertyService: PropertyService
|
||||
) {
|
||||
@ -31,6 +36,55 @@ export class ExchangeRateDataService {
|
||||
return this.currencyPairs;
|
||||
}
|
||||
|
||||
public async getExchangeRates({
|
||||
dateQuery,
|
||||
sourceCurrencies,
|
||||
destinationCurrency
|
||||
}: {
|
||||
dateQuery: DateQuery;
|
||||
sourceCurrencies: string[];
|
||||
destinationCurrency: string;
|
||||
}): Promise<DateBasedExchangeRate[]> {
|
||||
const symbols = [...sourceCurrencies, destinationCurrency]
|
||||
.map((currency) => `${baseCurrency}${currency}`)
|
||||
.filter((v, i, a) => a.indexOf(v) === i);
|
||||
const exchangeRates = await this.marketDataService.getRange({
|
||||
dateQuery,
|
||||
symbols
|
||||
});
|
||||
|
||||
if (exchangeRates.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const results: DateBasedExchangeRate[] = [];
|
||||
let currentDate = exchangeRates[0].date;
|
||||
let currentRates: { [symbol: string]: Big } = {};
|
||||
for (const exchangeRate of exchangeRates) {
|
||||
if (!isSameDay(currentDate, exchangeRate.date)) {
|
||||
results.push({
|
||||
date: currentDate,
|
||||
exchangeRates: this.getUserExchangeRates(
|
||||
currentRates,
|
||||
destinationCurrency,
|
||||
sourceCurrencies
|
||||
)
|
||||
});
|
||||
currentDate = exchangeRate.date;
|
||||
currentRates = {};
|
||||
}
|
||||
currentRates[exchangeRate.symbol] = new Big(exchangeRate.marketPrice);
|
||||
}
|
||||
results.push({
|
||||
date: currentDate,
|
||||
exchangeRates: this.getUserExchangeRates(
|
||||
currentRates,
|
||||
destinationCurrency,
|
||||
sourceCurrencies
|
||||
)
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
this.currencies = await this.prepareCurrencies();
|
||||
this.currencyPairs = [];
|
||||
@ -97,10 +151,10 @@ export class ExchangeRateDataService {
|
||||
this.exchangeRates[symbol] = resultExtended[symbol]?.[date]?.marketPrice;
|
||||
|
||||
if (!this.exchangeRates[symbol]) {
|
||||
// Not found, calculate indirectly via USD
|
||||
// Not found, calculate indirectly via base currency
|
||||
this.exchangeRates[symbol] =
|
||||
resultExtended[`${currency1}${'USD'}`]?.[date]?.marketPrice *
|
||||
resultExtended[`${'USD'}${currency2}`]?.[date]?.marketPrice;
|
||||
resultExtended[`${currency1}${baseCurrency}`]?.[date]?.marketPrice *
|
||||
resultExtended[`${baseCurrency}${currency2}`]?.[date]?.marketPrice;
|
||||
|
||||
// Calculate the opposite direction
|
||||
this.exchangeRates[`${currency2}${currency1}`] =
|
||||
@ -129,9 +183,9 @@ export class ExchangeRateDataService {
|
||||
if (this.exchangeRates[`${aFromCurrency}${aToCurrency}`]) {
|
||||
factor = this.exchangeRates[`${aFromCurrency}${aToCurrency}`];
|
||||
} else {
|
||||
// Calculate indirectly via USD
|
||||
const factor1 = this.exchangeRates[`${aFromCurrency}${'USD'}`];
|
||||
const factor2 = this.exchangeRates[`${'USD'}${aToCurrency}`];
|
||||
// Calculate indirectly via base currency
|
||||
const factor1 = this.exchangeRates[`${aFromCurrency}${baseCurrency}`];
|
||||
const factor2 = this.exchangeRates[`${baseCurrency}${aToCurrency}`];
|
||||
|
||||
factor = factor1 * factor2;
|
||||
|
||||
@ -194,6 +248,46 @@ export class ExchangeRateDataService {
|
||||
return uniq(currencies).sort();
|
||||
}
|
||||
|
||||
private getUserExchangeRates(
|
||||
currentRates: { [symbol: string]: Big },
|
||||
destinationCurrency: string,
|
||||
sourceCurrencies: string[]
|
||||
): { [currency: string]: Big } {
|
||||
const result: { [currency: string]: Big } = {};
|
||||
|
||||
for (const sourceCurrency of sourceCurrencies) {
|
||||
let exchangeRate: Big;
|
||||
if (sourceCurrency === destinationCurrency) {
|
||||
exchangeRate = new Big(1);
|
||||
} else if (
|
||||
destinationCurrency === baseCurrency &&
|
||||
currentRates[`${destinationCurrency}${sourceCurrency}`]
|
||||
) {
|
||||
exchangeRate = new Big(1).div(
|
||||
currentRates[`${destinationCurrency}${sourceCurrency}`]
|
||||
);
|
||||
} else if (
|
||||
sourceCurrency === baseCurrency &&
|
||||
currentRates[`${sourceCurrency}${destinationCurrency}`]
|
||||
) {
|
||||
exchangeRate = currentRates[`${sourceCurrency}${destinationCurrency}`];
|
||||
} else if (
|
||||
currentRates[`${baseCurrency}${destinationCurrency}`] &&
|
||||
currentRates[`${baseCurrency}${sourceCurrency}`]
|
||||
) {
|
||||
exchangeRate = currentRates[
|
||||
`${baseCurrency}${destinationCurrency}`
|
||||
].div(currentRates[`${baseCurrency}${sourceCurrency}`]);
|
||||
}
|
||||
|
||||
if (exchangeRate) {
|
||||
result[sourceCurrency] = exchangeRate;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private prepareCurrencyPairs(aCurrencies: string[]) {
|
||||
return aCurrencies
|
||||
.filter((currency) => {
|
||||
|
@ -0,0 +1,6 @@
|
||||
import Big from 'big.js';
|
||||
|
||||
export interface DateBasedExchangeRate {
|
||||
date: Date;
|
||||
exchangeRates: { [currency: string]: Big };
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user