Browse Source

Integrate dividend into transaction point concept

pull/3092/head
Thomas Kaul 2 years ago
parent
commit
67b29814b1
  1. 2
      apps/api/src/app/order/order.service.ts
  2. 1
      apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts
  3. 2
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts
  4. 2
      apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts
  5. 2
      apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  6. 2
      apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts
  7. 2
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  8. 2
      apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts
  9. 64
      apps/api/src/app/portfolio/portfolio-calculator.ts
  10. 22
      apps/api/src/app/portfolio/portfolio.controller.ts
  11. 64
      apps/api/src/app/portfolio/portfolio.service.ts
  12. 2
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  13. 1
      libs/common/src/lib/interfaces/portfolio-position.interface.ts
  14. 2
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  15. 2
      libs/common/src/lib/interfaces/symbol-metrics.interface.ts
  16. 2
      libs/common/src/lib/interfaces/timeline-position.interface.ts

2
apps/api/src/app/order/order.service.ts

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

1
apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts

@ -4,6 +4,7 @@ import Big from 'big.js';
export interface TransactionPointSymbol {
currency: string;
dataSource: DataSource;
dividend: Big;
fee: Big;
firstBuyDate: string;
investment: Big;

2
apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts

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

2
apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts

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

2
apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

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

2
apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts

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

2
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

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

2
apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts

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

64
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -70,6 +70,7 @@ export class PortfolioCalculator {
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
for (const order of this.orders) {
const currentDate = order.date;
@ -78,12 +79,13 @@ export class PortfolioCalculator {
const factor = getFactor(order.type);
const unitPrice = new Big(order.unitPrice);
if (oldAccumulatedSymbol) {
const newQuantity = order.quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
let investment = new Big(0);
let investment = oldAccumulatedSymbol.investment;
if (newQuantity.gt(0)) {
if (order.type === 'BUY') {
@ -104,6 +106,7 @@ export class PortfolioCalculator {
investment,
currency: order.currency,
dataSource: order.dataSource,
dividend: new Big(0),
fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity,
@ -115,6 +118,7 @@ export class PortfolioCalculator {
currentTransactionPointItem = {
currency: order.currency,
dataSource: order.dataSource,
dividend: new Big(0),
fee: order.fee,
firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor),
@ -583,6 +587,8 @@ export class PortfolioCalculator {
);
const {
dividend,
dividendInBaseCurrency,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
@ -608,6 +614,8 @@ export class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
positions.push({
dividend,
dividendInBaseCurrency,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
averagePrice: item.quantity.eq(0)
@ -842,6 +850,8 @@ export class PortfolioCalculator {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let dividend = new Big(0);
let dividendInBaseCurrency = new Big(0);
let fees = new Big(0);
let feesAtStartDate = new Big(0);
let feesAtStartDateWithCurrencyEffect = new Big(0);
@ -894,6 +904,8 @@ export class PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -934,6 +946,8 @@ export class PortfolioCalculator {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -1108,29 +1122,29 @@ export class PortfolioCalculator {
valueOfInvestmentBeforeTransactionWithCurrencyEffect;
}
const transactionInvestment =
order.type === 'BUY'
? order.quantity
let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0);
if (order.type === 'BUY') {
transactionInvestment = order.quantity
.mul(order.unitPriceInBaseCurrency)
.mul(getFactor(order.type))
: totalUnits.gt(0)
? totalInvestment
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect = order.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type));
} else if (order.type === 'SELL') {
if (totalUnits.gt(0)) {
transactionInvestment = totalInvestment
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type))
: new Big(0);
const transactionInvestmentWithCurrencyEffect =
order.type === 'BUY'
? order.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type))
: totalUnits.gt(0)
? totalInvestmentWithCurrencyEffect
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect
.div(totalUnits)
.mul(order.quantity)
.mul(getFactor(order.type))
: new Big(0);
.mul(getFactor(order.type));
}
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.toNumber());
@ -1186,6 +1200,13 @@ export class PortfolioCalculator {
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 valueOfInvestmentWithCurrencyEffect = totalUnits.mul(
@ -1277,7 +1298,7 @@ export class PortfolioCalculator {
grossPerformanceWithCurrencyEffect;
}
if (i > indexOfStartOrder) {
if (i > indexOfStartOrder && ['BUY', 'SELL'].includes(order.type)) {
// Only consider periods with an investment for the calculation of
// the time weighted investment
if (valueOfInvestmentBeforeTransaction.gt(0)) {
@ -1471,6 +1492,7 @@ export class PortfolioCalculator {
Time weighted investment with currency effect: ${timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect.toFixed(
2
)}
Total dividend: ${dividend.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed(
2
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
@ -1495,6 +1517,8 @@ export class PortfolioCalculator {
return {
currentValues,
currentValuesWithCurrencyEffect,
dividend,
dividendInBaseCurrency,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
initialValue,

22
apps/api/src/app/portfolio/portfolio.controller.ts

@ -1,4 +1,5 @@
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 { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import {
@ -11,6 +12,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import {
DEFAULT_CURRENCY,
HEADER_KEY_IMPERSONATION
@ -57,6 +59,8 @@ export class PortfolioController {
private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
@ -161,7 +165,7 @@ export class PortfolioController {
'currentNetPerformance',
'currentNetPerformanceWithCurrencyEffect',
'currentValue',
'dividend',
'dividendInBaseCurrency',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
@ -231,11 +235,21 @@ export class PortfolioController {
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({
activities,
dateRange,
filters,
groupBy,
impersonationId
groupBy
});
if (

64
apps/api/src/app/portfolio/portfolio.service.ts

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

2
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

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

1
libs/common/src/lib/interfaces/portfolio-position.interface.ts

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

2
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

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

2
libs/common/src/lib/interfaces/symbol-metrics.interface.ts

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

2
libs/common/src/lib/interfaces/timeline-position.interface.ts

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

Loading…
Cancel
Save