Feature/integrate dividend into transaction point concept (#3092)

* Integrate dividend into transaction point concept

* Update changelog
pull/3115/head
Thomas Kaul 7 months ago committed by GitHub
parent 77358eed65
commit 07661d9262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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
- Integrated dividend into the transaction point concept in the portfolio service
## 2.61.1 - 2024-03-06 ## 2.61.1 - 2024-03-06
### Fixed ### Fixed

@ -13,7 +13,6 @@ export default {
}, },
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/api', coverageDirectory: '../../coverage/apps/api',
testTimeout: 10000,
testEnvironment: 'node', testEnvironment: 'node',
preset: '../../jest.preset.js' preset: '../../jest.preset.js'
}; };

@ -340,11 +340,13 @@ export class OrderService {
return { return {
...order, ...order,
value, value,
// TODO: Use exchange rate of date
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee, order.fee,
order.SymbolProfile.currency, order.SymbolProfile.currency,
userCurrency userCurrency
), ),
// TODO: Use exchange rate of date
valueInBaseCurrency: this.exchangeRateDataService.toCurrency( valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value, value,
order.SymbolProfile.currency, order.SymbolProfile.currency,

@ -43,6 +43,15 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 }; return { marketPrice: 0 };
case 'MSFT':
if (isSameDay(parseDate('2021-09-16'), date)) {
return { marketPrice: 89.12 };
} else if (isSameDay(parseDate('2023-07-10'), date)) {
return { marketPrice: 331.83 };
}
return { marketPrice: 0 };
case 'NOVN.SW': case 'NOVN.SW':
if (isSameDay(parseDate('2022-04-11'), date)) { if (isSameDay(parseDate('2022-04-11'), date)) {
return { marketPrice: 87.8 }; return { marketPrice: 87.8 };

@ -2,8 +2,10 @@ import { DataSource, Tag } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
export interface TransactionPointSymbol { export interface TransactionPointSymbol {
averagePrice: Big;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
dividend: Big;
fee: Big; fee: Big;
firstBuyDate: string; firstBuyDate: string;
investment: Big; investment: Big;

@ -107,6 +107,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
firstBuyDate: '2021-11-22', firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),

@ -96,6 +96,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('136.6'), averagePrice: new Big('136.6'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1.55'), fee: new Big('1.55'),
firstBuyDate: '2021-11-30', firstBuyDate: '2021-11-30',
grossPerformance: new Big('24.6'), grossPerformance: new Big('24.6'),

@ -120,6 +120,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('320.43'), averagePrice: new Big('320.43'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
firstBuyDate: '2015-01-01', firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74'), grossPerformance: new Big('27172.74'),

@ -109,6 +109,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('89.12'), averagePrice: new Big('89.12'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1'), fee: new Big('1'),
firstBuyDate: '2023-01-03', firstBuyDate: '2023-01-03',
grossPerformance: new Big('27.33'), grossPerformance: new Big('27.33'),

@ -0,0 +1,118 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
});
describe('get current positions', () => {
it.only('with MSFT buy', async () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
exchangeRateDataService,
currency: 'USD',
orders: [
{
currency: 'USD',
date: '2021-09-16',
dataSource: 'YAHOO',
fee: new Big(19),
name: 'Microsoft Inc.',
quantity: new Big(1),
symbol: 'MSFT',
type: 'BUY',
unitPrice: new Big(298.58)
},
{
currency: 'USD',
date: '2021-11-16',
dataSource: 'YAHOO',
fee: new Big(0),
name: 'Microsoft Inc.',
quantity: new Big(1),
symbol: 'MSFT',
type: 'DIVIDEND',
unitPrice: new Big(0.62)
}
]
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2023-07-10').getTime());
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2023-07-10')
);
spy.mockRestore();
expect(currentPositions).toMatchObject({
errors: [],
hasErrors: false,
positions: [
{
averagePrice: new Big('298.58'),
currency: 'USD',
dataSource: 'YAHOO',
dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'),
fee: new Big('19'),
firstBuyDate: '2021-09-16',
investment: new Big('298.58'),
investmentWithCurrencyEffect: new Big('298.58'),
marketPrice: 331.83,
marketPriceInBaseCurrency: 331.83,
quantity: new Big('1'),
symbol: 'MSFT',
tags: undefined,
transactionCount: 2
}
],
totalInvestment: new Big('298.58'),
totalInvestmentWithCurrencyEffect: new Big('298.58')
});
});
});
});

@ -107,6 +107,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('75.80'), averagePrice: new Big('75.80'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.25'), fee: new Big('4.25'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),

@ -133,6 +133,8 @@ describe('PortfolioCalculator', () => {
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
firstBuyDate: '2022-03-07', firstBuyDate: '2022-03-07',
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),

@ -70,6 +70,7 @@ export class PortfolioCalculator {
let lastDate: string = null; let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null; let lastTransactionPoint: TransactionPoint = null;
for (const order of this.orders) { for (const order of this.orders) {
const currentDate = order.date; const currentDate = order.date;
@ -77,33 +78,32 @@ export class PortfolioCalculator {
const oldAccumulatedSymbol = symbols[order.symbol]; const oldAccumulatedSymbol = symbols[order.symbol];
const factor = getFactor(order.type); const factor = getFactor(order.type);
const unitPrice = new Big(order.unitPrice);
if (oldAccumulatedSymbol) { if (oldAccumulatedSymbol) {
let investment = oldAccumulatedSymbol.investment;
const newQuantity = order.quantity const newQuantity = order.quantity
.mul(factor) .mul(factor)
.plus(oldAccumulatedSymbol.quantity); .plus(oldAccumulatedSymbol.quantity);
let investment = new Big(0); if (order.type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
if (newQuantity.gt(0)) { order.quantity.mul(order.unitPrice)
if (order.type === 'BUY') { );
investment = oldAccumulatedSymbol.investment.plus( } else if (order.type === 'SELL') {
order.quantity.mul(unitPrice) investment = oldAccumulatedSymbol.investment.minus(
); order.quantity.mul(oldAccumulatedSymbol.averagePrice)
} else if (order.type === 'SELL') { );
const averagePrice = oldAccumulatedSymbol.investment.div(
oldAccumulatedSymbol.quantity
);
investment = oldAccumulatedSymbol.investment.minus(
order.quantity.mul(averagePrice)
);
}
} }
currentTransactionPointItem = { currentTransactionPointItem = {
investment, investment,
averagePrice: newQuantity.gt(0)
? investment.div(newQuantity)
: new Big(0),
currency: order.currency, currency: order.currency,
dataSource: order.dataSource, dataSource: order.dataSource,
dividend: new Big(0),
fee: order.fee.plus(oldAccumulatedSymbol.fee), fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity, quantity: newQuantity,
@ -113,11 +113,13 @@ export class PortfolioCalculator {
}; };
} else { } else {
currentTransactionPointItem = { currentTransactionPointItem = {
averagePrice: order.unitPrice,
currency: order.currency, currency: order.currency,
dataSource: order.dataSource, dataSource: order.dataSource,
dividend: new Big(0),
fee: order.fee, fee: order.fee,
firstBuyDate: order.date, firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor), investment: order.unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor), quantity: order.quantity.mul(factor),
symbol: order.symbol, symbol: order.symbol,
tags: order.tags, tags: order.tags,
@ -128,22 +130,28 @@ export class PortfolioCalculator {
symbols[order.symbol] = currentTransactionPointItem; symbols[order.symbol] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? []; const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter( const newItems = items.filter(
(transactionPointItem) => transactionPointItem.symbol !== order.symbol (transactionPointItem) => transactionPointItem.symbol !== order.symbol
); );
newItems.push(currentTransactionPointItem); newItems.push(currentTransactionPointItem);
newItems.sort((a, b) => { newItems.sort((a, b) => {
return a.symbol?.localeCompare(b.symbol); return a.symbol?.localeCompare(b.symbol);
}); });
if (lastDate !== currentDate || lastTransactionPoint === null) { if (lastDate !== currentDate || lastTransactionPoint === null) {
lastTransactionPoint = { lastTransactionPoint = {
date: currentDate, date: currentDate,
items: newItems items: newItems
}; };
this.transactionPoints.push(lastTransactionPoint); this.transactionPoints.push(lastTransactionPoint);
} else { } else {
lastTransactionPoint.items = newItems; lastTransactionPoint.items = newItems;
} }
lastDate = currentDate; lastDate = currentDate;
} }
} }
@ -583,6 +591,8 @@ export class PortfolioCalculator {
); );
const { const {
dividend,
dividendInBaseCurrency,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
@ -608,11 +618,11 @@ export class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
positions.push({ positions.push({
dividend,
dividendInBaseCurrency,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
averagePrice: item.quantity.eq(0) averagePrice: item.averagePrice,
? new Big(0)
: item.investment.div(item.quantity),
currency: item.currency, currency: item.currency,
dataSource: item.dataSource, dataSource: item.dataSource,
fee: item.fee, fee: item.fee,
@ -842,6 +852,8 @@ export class PortfolioCalculator {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {}; const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let dividend = new Big(0);
let dividendInBaseCurrency = new Big(0);
let fees = new Big(0); let fees = new Big(0);
let feesAtStartDate = new Big(0); let feesAtStartDate = new Big(0);
let feesAtStartDateWithCurrencyEffect = new Big(0); let feesAtStartDateWithCurrencyEffect = new Big(0);
@ -894,6 +906,8 @@ export class PortfolioCalculator {
return { return {
currentValues: {}, currentValues: {},
currentValuesWithCurrencyEffect: {}, currentValuesWithCurrencyEffect: {},
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -934,6 +948,8 @@ export class PortfolioCalculator {
return { return {
currentValues: {}, currentValues: {},
currentValuesWithCurrencyEffect: {}, currentValuesWithCurrencyEffect: {},
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -1108,29 +1124,29 @@ export class PortfolioCalculator {
valueOfInvestmentBeforeTransactionWithCurrencyEffect; valueOfInvestmentBeforeTransactionWithCurrencyEffect;
} }
const transactionInvestment = let transactionInvestment = new Big(0);
order.type === 'BUY' let transactionInvestmentWithCurrencyEffect = new Big(0);
? order.quantity
.mul(order.unitPriceInBaseCurrency) if (order.type === 'BUY') {
.mul(getFactor(order.type)) transactionInvestment = order.quantity
: totalUnits.gt(0) .mul(order.unitPriceInBaseCurrency)
? totalInvestment .mul(getFactor(order.type));
.div(totalUnits) transactionInvestmentWithCurrencyEffect = order.quantity
.mul(order.quantity) .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type)) .mul(getFactor(order.type));
: new Big(0); } else if (order.type === 'SELL') {
if (totalUnits.gt(0)) {
const transactionInvestmentWithCurrencyEffect = transactionInvestment = totalInvestment
order.type === 'BUY' .div(totalUnits)
? order.quantity .mul(order.quantity)
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) .mul(getFactor(order.type));
.mul(getFactor(order.type)) transactionInvestmentWithCurrencyEffect =
: totalUnits.gt(0) totalInvestmentWithCurrencyEffect
? totalInvestmentWithCurrencyEffect .div(totalUnits)
.div(totalUnits) .mul(order.quantity)
.mul(order.quantity) .mul(getFactor(order.type));
.mul(getFactor(order.type)) }
: new Big(0); }
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.toNumber()); console.log('totalInvestment', totalInvestment.toNumber());
@ -1186,6 +1202,13 @@ export class PortfolioCalculator {
totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type))); totalUnits = totalUnits.plus(order.quantity.mul(getFactor(order.type)));
if (order.type === 'DIVIDEND') {
dividend = dividend.plus(order.quantity.mul(order.unitPrice));
dividendInBaseCurrency = dividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1)
);
}
const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency); const valueOfInvestment = totalUnits.mul(order.unitPriceInBaseCurrency);
const valueOfInvestmentWithCurrencyEffect = totalUnits.mul( const valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
@ -1277,7 +1300,7 @@ export class PortfolioCalculator {
grossPerformanceWithCurrencyEffect; grossPerformanceWithCurrencyEffect;
} }
if (i > indexOfStartOrder) { if (i > indexOfStartOrder && ['BUY', 'SELL'].includes(order.type)) {
// Only consider periods with an investment for the calculation of // Only consider periods with an investment for the calculation of
// the time weighted investment // the time weighted investment
if (valueOfInvestmentBeforeTransaction.gt(0)) { if (valueOfInvestmentBeforeTransaction.gt(0)) {
@ -1471,6 +1494,7 @@ export class PortfolioCalculator {
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed( Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
2 2
)} )}
Total dividend: ${dividend.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed( Gross performance: ${totalGrossPerformance.toFixed(
2 2
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% )} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
@ -1495,6 +1519,8 @@ export class PortfolioCalculator {
return { return {
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
dividend,
dividendInBaseCurrency,
grossPerformancePercentage, grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
initialValue, initialValue,

@ -1,4 +1,5 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { import {
@ -11,6 +12,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
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 { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
HEADER_KEY_IMPERSONATION HEADER_KEY_IMPERSONATION
@ -57,6 +59,8 @@ export class PortfolioController {
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService private readonly userService: UserService
@ -161,7 +165,7 @@ export class PortfolioController {
'currentNetPerformance', 'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect', 'currentNetPerformanceWithCurrencyEffect',
'currentValue', 'currentValue',
'dividend', 'dividendInBaseCurrency',
'emergencyFund', 'emergencyFund',
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
@ -231,11 +235,21 @@ export class PortfolioController {
filterByTags filterByTags
}); });
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
const { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId: impersonationUserId || this.request.user.id,
types: ['DIVIDEND']
});
let dividends = await this.portfolioService.getDividends({ let dividends = await this.portfolioService.getDividends({
activities,
dateRange, dateRange,
filters, groupBy
groupBy,
impersonationId
}); });
if ( if (

@ -218,27 +218,14 @@ export class PortfolioService {
} }
public async getDividends({ public async getDividends({
dateRange, activities,
filters, dateRange = 'max',
groupBy, groupBy
impersonationId
}: { }: {
dateRange: DateRange; activities: Activity[];
filters?: Filter[]; dateRange?: DateRange;
groupBy?: GroupBy; groupBy?: GroupBy;
impersonationId: string;
}): Promise<InvestmentItem[]> { }): Promise<InvestmentItem[]> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId,
types: ['DIVIDEND']
});
let dividends = activities.map(({ date, valueInBaseCurrency }) => { let dividends = activities.map(({ date, valueInBaseCurrency }) => {
return { return {
date: format(date, DATE_FORMAT), date: format(date, DATE_FORMAT),
@ -441,6 +428,7 @@ export class PortfolioService {
for (const { for (const {
currency, currency,
dividend,
firstBuyDate, firstBuyDate,
grossPerformance, grossPerformance,
grossPerformanceWithCurrencyEffect, grossPerformanceWithCurrencyEffect,
@ -553,6 +541,7 @@ export class PortfolioService {
countries: symbolProfile.countries, countries: symbolProfile.countries,
dataSource: symbolProfile.dataSource, dataSource: symbolProfile.dataSource,
dateOfFirstActivity: parseDate(firstBuyDate), dateOfFirstActivity: parseDate(firstBuyDate),
dividend: dividend?.toNumber() ?? 0,
grossPerformance: grossPerformance?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0,
grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0, grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0,
grossPerformancePercentWithCurrencyEffect: grossPerformancePercentWithCurrencyEffect:
@ -723,7 +712,7 @@ export class PortfolioService {
.filter((order) => { .filter((order) => {
tags = tags.concat(order.tags); tags = tags.concat(order.tags);
return ['BUY', 'ITEM', 'SELL'].includes(order.type); return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type);
}) })
.map((order) => ({ .map((order) => ({
currency: order.SymbolProfile.currency, currency: order.SymbolProfile.currency,
@ -764,6 +753,7 @@ export class PortfolioService {
averagePrice, averagePrice,
currency, currency,
dataSource, dataSource,
dividendInBaseCurrency,
fee, fee,
firstBuyDate, firstBuyDate,
marketPrice, marketPrice,
@ -780,16 +770,6 @@ export class PortfolioService {
return Account; return Account;
}); });
const dividendInBaseCurrency = getSum(
orders
.filter(({ type }) => {
return type === 'DIVIDEND';
})
.map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency);
})
);
const historicalData = await this.dataProviderService.getHistorical( const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol: aSymbol }], [{ dataSource, symbol: aSymbol }],
'day', 'day',
@ -823,9 +803,7 @@ export class PortfolioService {
); );
if (currentSymbol) { if (currentSymbol) {
currentAveragePrice = currentSymbol.quantity.eq(0) currentAveragePrice = currentSymbol.averagePrice.toNumber();
? 0
: currentSymbol.investment.div(currentSymbol.quantity).toNumber();
currentQuantity = currentSymbol.quantity.toNumber(); currentQuantity = currentSymbol.quantity.toNumber();
} }
@ -1636,6 +1614,7 @@ export class PortfolioService {
countries: [], countries: [],
dataSource: undefined, dataSource: undefined,
dateOfFirstActivity: undefined, dateOfFirstActivity: undefined,
dividend: 0,
grossPerformance: 0, grossPerformance: 0,
grossPerformancePercent: 0, grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0, grossPerformancePercentWithCurrencyEffect: 0,
@ -1765,11 +1744,16 @@ export class PortfolioService {
} }
} }
const dividend = this.getSumOfActivityType({ const dividendInBaseCurrency = (
activities, await this.getDividends({
userCurrency, activities: activities.filter(({ type }) => {
activityType: 'DIVIDEND' return type === 'DIVIDEND';
}).toNumber(); })
})
).reduce(
(previous, current) => new Big(previous).plus(current.investment),
new Big(0)
);
const emergencyFund = new Big( const emergencyFund = new Big(
Math.max( Math.max(
@ -1904,7 +1888,6 @@ export class PortfolioService {
annualizedPerformancePercent, annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect, annualizedPerformancePercentWithCurrencyEffect,
cash, cash,
dividend,
excludedAccountsAndActivities, excludedAccountsAndActivities,
fees, fees,
firstOrderDate, firstOrderDate,
@ -1915,6 +1898,7 @@ export class PortfolioService {
totalBuy, totalBuy,
totalSell, totalSell,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
emergencyFund: { emergencyFund: {
assets: emergencyFundPositionsValueInBaseCurrency, assets: emergencyFundPositionsValueInBaseCurrency,
cash: emergencyFund cash: emergencyFund
@ -1967,7 +1951,7 @@ export class PortfolioService {
private async getTransactionPoints({ private async getTransactionPoints({
filters, filters,
includeDrafts = false, includeDrafts = false,
types = ['BUY', 'ITEM', 'LIABILITY', 'SELL'], types = ['BUY', 'DIVIDEND', 'ITEM', 'LIABILITY', 'SELL'],
userId, userId,
withExcludedAccounts = false withExcludedAccounts = false
}: { }: {
@ -2144,14 +2128,11 @@ export class PortfolioService {
type type
} of ordersByAccount) { } of ordersByAccount) {
let currentValueOfSymbolInBaseCurrency = let currentValueOfSymbolInBaseCurrency =
getFactor(type) *
quantity * quantity *
(portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? (portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ??
0); 0);
if (['LIABILITY', 'SELL'].includes(type)) {
currentValueOfSymbolInBaseCurrency *= getFactor(type);
}
if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { if (accounts[Account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) {
accounts[Account?.id || UNKNOWN_KEY].valueInBaseCurrency += accounts[Account?.id || UNKNOWN_KEY].valueInBaseCurrency +=
currentValueOfSymbolInBaseCurrency; currentValueOfSymbolInBaseCurrency;

@ -22,6 +22,13 @@ export const ExchangeRateDataServiceMock = {
'2023-07-10': 0.8854 '2023-07-10': 0.8854
} }
}); });
} else if (targetCurrency === 'USD') {
return Promise.resolve({
USDUSD: {
'2018-01-01': 1,
'2023-07-10': 1
}
});
} }
return Promise.resolve({}); return Promise.resolve({});

@ -328,7 +328,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.dividend" [value]="isLoading ? undefined : summary?.dividendInBaseCurrency"
/> />
</div> </div>
</div> </div>

@ -14,6 +14,7 @@ export interface PortfolioPosition {
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
dateOfFirstActivity: Date; dateOfFirstActivity: Date;
dividend: number;
exchange?: string; exchange?: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;

@ -5,7 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercentWithCurrencyEffect: number; annualizedPerformancePercentWithCurrencyEffect: number;
cash: number; cash: number;
committedFunds: number; committedFunds: number;
dividend: number; dividendInBaseCurrency: number;
emergencyFund: { emergencyFund: {
assets: number; assets: number;
cash: number; cash: number;

@ -7,6 +7,8 @@ export interface SymbolMetrics {
currentValuesWithCurrencyEffect: { currentValuesWithCurrencyEffect: {
[date: string]: Big; [date: string]: Big;
}; };
dividend: Big;
dividendInBaseCurrency: Big;
grossPerformance: Big; grossPerformance: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big; grossPerformancePercentageWithCurrencyEffect: Big;

@ -5,6 +5,8 @@ export interface TimelinePosition {
averagePrice: Big; averagePrice: Big;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
dividend: Big;
dividendInBaseCurrency: Big;
fee: Big; fee: Big;
firstBuyDate: string; firstBuyDate: string;
grossPerformance: Big; grossPerformance: Big;

Loading…
Cancel
Save