Browse Source

Bugfix/improve account calculations (#737)

* Improve account calculations

* Update changelog
pull/739/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
3de7d3f60e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 8
      apps/api/src/app/account/account.controller.ts
  3. 22
      apps/api/src/app/account/account.service.ts
  4. 2
      apps/api/src/app/account/interfaces/cash-details.interface.ts
  5. 61
      apps/api/src/app/portfolio/portfolio.service-new.ts
  6. 61
      apps/api/src/app/portfolio/portfolio.service.ts
  7. 6
      apps/client/src/app/components/accounts-table/accounts-table.component.html
  8. 4
      apps/client/src/app/components/accounts-table/accounts-table.component.ts
  9. 33
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  10. 4
      apps/client/src/app/pages/accounts/accounts-page.html
  11. 4
      libs/common/src/lib/interfaces/accounts.interface.ts
  12. 3
      libs/common/src/lib/types/account-with-value.type.ts

6
CHANGELOG.md

@ -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
### Fixed
- Improved the account calculations
## 1.122.0 - 01.03.2022 ## 1.122.0 - 01.03.2022
### Added ### Added

8
apps/api/src/app/account/account.controller.ts

@ -101,16 +101,18 @@ export class AccountController {
) { ) {
accountsWithAggregations = { accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [ ...nullifyValuesInObject(accountsWithAggregations, [
'totalBalance', 'totalBalanceInBaseCurrency',
'totalValue' 'totalValueInBaseCurrency'
]), ]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [ accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance', 'balance',
'balanceInBaseCurrency',
'convertedBalance', 'convertedBalance',
'fee', 'fee',
'quantity', 'quantity',
'unitPrice', 'unitPrice',
'value' 'value',
'valueInBaseCurrency'
]) ])
}; };
} }

22
apps/api/src/app/account/account.service.ts

@ -2,6 +2,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Account, Order, Platform, Prisma } from '@prisma/client'; import { Account, Order, Platform, Prisma } from '@prisma/client';
import Big from 'big.js';
import { CashDetails } from './interfaces/cash-details.interface'; import { CashDetails } from './interfaces/cash-details.interface';
@ -105,21 +106,26 @@ export class AccountService {
aUserId: string, aUserId: string,
aCurrency: string aCurrency: string
): Promise<CashDetails> { ): Promise<CashDetails> {
let totalCashBalance = 0; let totalCashBalanceInBaseCurrency = new Big(0);
const accounts = await this.accounts({ const accounts = await this.accounts({
where: { userId: aUserId } where: { userId: aUserId }
}); });
accounts.forEach((account) => { for (const account of accounts) {
totalCashBalance += this.exchangeRateDataService.toCurrency( totalCashBalanceInBaseCurrency = totalCashBalanceInBaseCurrency.plus(
account.balance, this.exchangeRateDataService.toCurrency(
account.currency, account.balance,
aCurrency account.currency,
aCurrency
)
); );
}); }
return { accounts, balance: totalCashBalance }; return {
accounts,
balanceInBaseCurrency: totalCashBalanceInBaseCurrency.toNumber()
};
} }
public async updateAccount( public async updateAccount(

2
apps/api/src/app/account/interfaces/cash-details.interface.ts

@ -2,5 +2,5 @@ import { Account } from '@prisma/client';
export interface CashDetails { export interface CashDetails {
accounts: Account[]; accounts: Account[];
balance: number; balanceInBaseCurrency: number;
} }

61
apps/api/src/app/portfolio/portfolio.service-new.ts

@ -100,15 +100,22 @@ export class PortfolioServiceNew {
} }
} }
const value = details.accounts[account.id]?.current ?? 0;
const result = { const result = {
...account, ...account,
transactionCount, transactionCount,
convertedBalance: this.exchangeRateDataService.toCurrency( value,
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
account.balance, account.balance,
account.currency, account.currency,
userCurrency userCurrency
), ),
value: details.accounts[account.id]?.current ?? 0 valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
account.currency,
userCurrency
)
}; };
delete result.Order; delete result.Order;
@ -119,17 +126,26 @@ export class PortfolioServiceNew {
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> { public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId); const accounts = await this.getAccounts(aUserId);
let totalBalance = 0; let totalBalanceInBaseCurrency = new Big(0);
let totalValue = 0; let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
for (const account of accounts) { for (const account of accounts) {
totalBalance += account.convertedBalance; totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
totalValue += account.value; account.balanceInBaseCurrency
);
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency
);
transactionCount += account.transactionCount; transactionCount += account.transactionCount;
} }
return { accounts, totalBalance, totalValue, transactionCount }; return {
accounts,
transactionCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber()
};
} }
public async getInvestments( public async getInvestments(
@ -293,13 +309,11 @@ export class PortfolioServiceNew {
orders: portfolioOrders orders: portfolioOrders
}); });
if (transactionPoints?.length <= 0) {
return { accounts: {}, holdings: {}, hasErrors: false };
}
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
);
const startDate = this.getStartDate(aDateRange, portfolioStart); const startDate = this.getStartDate(aDateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate startDate
@ -312,9 +326,11 @@ export class PortfolioServiceNew {
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = {};
const totalInvestment = currentPositions.totalInvestment.plus( const totalInvestment = currentPositions.totalInvestment.plus(
cashDetails.balance cashDetails.balanceInBaseCurrency
);
const totalValue = currentPositions.currentValue.plus(
cashDetails.balanceInBaseCurrency
); );
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
const dataGatheringItems = currentPositions.positions.map((position) => { const dataGatheringItems = currentPositions.positions.map((position) => {
return { return {
@ -869,7 +885,7 @@ export class PortfolioServiceNew {
const performanceInformation = await this.getPerformance(aImpersonationId); const performanceInformation = await this.getPerformance(aImpersonationId);
const { balance } = await this.accountService.getCashDetails( const { balanceInBaseCurrency } = await this.accountService.getCashDetails(
userId, userId,
userCurrency userCurrency
); );
@ -887,7 +903,7 @@ export class PortfolioServiceNew {
const committedFunds = new Big(totalBuy).minus(totalSell); const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balance) const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue) .plus(performanceInformation.performance.currentValue)
.plus(items) .plus(items)
.toNumber(); .toNumber();
@ -917,7 +933,7 @@ export class PortfolioServiceNew {
netWorth, netWorth,
totalBuy, totalBuy,
totalSell, totalSell,
cash: balance, cash: balanceInBaseCurrency,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.filter((order) => { ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';
@ -1153,17 +1169,12 @@ export class PortfolioServiceNew {
return accountId === account.id; return accountId === account.id;
}); });
const convertedBalance = this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
);
accounts[account.id] = { accounts[account.id] = {
balance: convertedBalance, balance: account.balance,
currency: account.currency, currency: account.currency,
current: convertedBalance, current: account.balance,
name: account.name, name: account.name,
original: convertedBalance original: account.balance
}; };
for (const order of ordersByAccount) { for (const order of ordersByAccount) {

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

@ -99,15 +99,22 @@ export class PortfolioService {
} }
} }
const value = details.accounts[account.id]?.current ?? 0;
const result = { const result = {
...account, ...account,
transactionCount, transactionCount,
convertedBalance: this.exchangeRateDataService.toCurrency( value,
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
account.balance, account.balance,
account.currency, account.currency,
userCurrency userCurrency
), ),
value: details.accounts[account.id]?.current ?? 0 valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
account.currency,
userCurrency
)
}; };
delete result.Order; delete result.Order;
@ -118,17 +125,26 @@ export class PortfolioService {
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> { public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId); const accounts = await this.getAccounts(aUserId);
let totalBalance = 0; let totalBalanceInBaseCurrency = new Big(0);
let totalValue = 0; let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
for (const account of accounts) { for (const account of accounts) {
totalBalance += account.convertedBalance; totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
totalValue += account.value; account.balanceInBaseCurrency
);
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency
);
transactionCount += account.transactionCount; transactionCount += account.transactionCount;
} }
return { accounts, totalBalance, totalValue, transactionCount }; return {
accounts,
transactionCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalValueInBaseCurrency: totalValueInBaseCurrency.toNumber()
};
} }
public async getInvestments( public async getInvestments(
@ -281,13 +297,11 @@ export class PortfolioService {
userId userId
}); });
if (transactionPoints?.length <= 0) {
return { accounts: {}, holdings: {}, hasErrors: false };
}
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
);
const startDate = this.getStartDate(aDateRange, portfolioStart); const startDate = this.getStartDate(aDateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate startDate
@ -300,9 +314,11 @@ export class PortfolioService {
const holdings: PortfolioDetails['holdings'] = {}; const holdings: PortfolioDetails['holdings'] = {};
const totalInvestment = currentPositions.totalInvestment.plus( const totalInvestment = currentPositions.totalInvestment.plus(
cashDetails.balance cashDetails.balanceInBaseCurrency
);
const totalValue = currentPositions.currentValue.plus(
cashDetails.balanceInBaseCurrency
); );
const totalValue = currentPositions.currentValue.plus(cashDetails.balance);
const dataGatheringItems = currentPositions.positions.map((position) => { const dataGatheringItems = currentPositions.positions.map((position) => {
return { return {
@ -848,7 +864,7 @@ export class PortfolioService {
const performanceInformation = await this.getPerformance(aImpersonationId); const performanceInformation = await this.getPerformance(aImpersonationId);
const { balance } = await this.accountService.getCashDetails( const { balanceInBaseCurrency } = await this.accountService.getCashDetails(
userId, userId,
userCurrency userCurrency
); );
@ -866,7 +882,7 @@ export class PortfolioService {
const committedFunds = new Big(totalBuy).minus(totalSell); const committedFunds = new Big(totalBuy).minus(totalSell);
const netWorth = new Big(balance) const netWorth = new Big(balanceInBaseCurrency)
.plus(performanceInformation.performance.currentValue) .plus(performanceInformation.performance.currentValue)
.plus(items) .plus(items)
.toNumber(); .toNumber();
@ -882,7 +898,7 @@ export class PortfolioService {
totalSell, totalSell,
annualizedPerformancePercent: annualizedPerformancePercent:
performanceInformation.performance.annualizedPerformancePercent, performanceInformation.performance.annualizedPerformancePercent,
cash: balance, cash: balanceInBaseCurrency,
committedFunds: committedFunds.toNumber(), committedFunds: committedFunds.toNumber(),
ordersCount: orders.filter((order) => { ordersCount: orders.filter((order) => {
return order.type === 'BUY' || order.type === 'SELL'; return order.type === 'BUY' || order.type === 'SELL';
@ -1113,17 +1129,12 @@ export class PortfolioService {
return accountId === account.id; return accountId === account.id;
}); });
const convertedBalance = this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
userCurrency
);
accounts[account.id] = { accounts[account.id] = {
balance: convertedBalance, balance: account.balance,
currency: account.currency, currency: account.currency,
current: convertedBalance, current: account.balance,
name: account.name, name: account.name,
original: convertedBalance original: account.balance
}; };
for (const order of ordersByAccount) { for (const order of ordersByAccount) {

6
apps/client/src/app/components/accounts-table/accounts-table.component.html

@ -86,7 +86,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="element.convertedBalance" [value]="element.balance"
></gf-value> ></gf-value>
</td> </td>
<td *matFooterCellDef class="px-1 text-right" mat-footer-cell> <td *matFooterCellDef class="px-1 text-right" mat-footer-cell>
@ -94,7 +94,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalBalance" [value]="totalBalanceInBaseCurrency"
></gf-value> ></gf-value>
</td> </td>
</ng-container> </ng-container>
@ -116,7 +116,7 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="totalValue" [value]="totalValueInBaseCurrency"
></gf-value> ></gf-value>
</td> </td>
</ng-container> </ng-container>

4
apps/client/src/app/components/accounts-table/accounts-table.component.ts

@ -24,8 +24,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() deviceType: string; @Input() deviceType: string;
@Input() locale: string; @Input() locale: string;
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() totalBalance: number; @Input() totalBalanceInBaseCurrency: number;
@Input() totalValue: number; @Input() totalValueInBaseCurrency: number;
@Input() transactionCount: number; @Input() transactionCount: number;
@Output() accountDeleted = new EventEmitter<string>(); @Output() accountDeleted = new EventEmitter<string>();

33
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -28,8 +28,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public hasPermissionToCreateAccount: boolean; public hasPermissionToCreateAccount: boolean;
public hasPermissionToDeleteAccount: boolean; public hasPermissionToDeleteAccount: boolean;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public totalBalance = 0; public totalBalanceInBaseCurrency = 0;
public totalValue = 0; public totalValueInBaseCurrency = 0;
public transactionCount = 0; public transactionCount = 0;
public user: User; public user: User;
@ -106,18 +106,25 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchAccounts() .fetchAccounts()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accounts, totalBalance, totalValue, transactionCount }) => { .subscribe(
this.accounts = accounts; ({
this.totalBalance = totalBalance; accounts,
this.totalValue = totalValue; totalBalanceInBaseCurrency,
this.transactionCount = transactionCount; totalValueInBaseCurrency,
transactionCount
if (this.accounts?.length <= 0) { }) => {
this.router.navigate([], { queryParams: { createDialog: true } }); this.accounts = accounts;
} this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency;
this.transactionCount = transactionCount;
if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); }
);
} }
public onDeleteAccount(aId: string) { public onDeleteAccount(aId: string) {

4
apps/client/src/app/pages/accounts/accounts-page.html

@ -9,8 +9,8 @@
[deviceType]="deviceType" [deviceType]="deviceType"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView" [showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
[totalBalance]="totalBalance" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValue]="totalValue" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount" [transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)" (accountToUpdate)="onUpdateAccount($event)"

4
libs/common/src/lib/interfaces/accounts.interface.ts

@ -2,7 +2,7 @@ import { AccountWithValue } from '@ghostfolio/common/types';
export interface Accounts { export interface Accounts {
accounts: AccountWithValue[]; accounts: AccountWithValue[];
totalBalance: number; totalBalanceInBaseCurrency: number;
totalValue: number; totalValueInBaseCurrency: number;
transactionCount: number; transactionCount: number;
} }

3
libs/common/src/lib/types/account-with-value.type.ts

@ -1,7 +1,8 @@
import { Account as AccountModel } from '@prisma/client'; import { Account as AccountModel } from '@prisma/client';
export type AccountWithValue = AccountModel & { export type AccountWithValue = AccountModel & {
convertedBalance: number; balanceInBaseCurrency: number;
transactionCount: number; transactionCount: number;
value: number; value: number;
valueInBaseCurrency: number;
}; };

Loading…
Cancel
Save