From bcb7f5f52211029f700a1abaab4413810b60318e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 24 Jan 2022 21:38:59 +0100 Subject: [PATCH] Feature/add feature toggle for new calculation engine (#649) * Add feature toggle for new calculation engine * Update changelog --- CHANGELOG.md | 4 +++ .../api/src/app/account/account.controller.ts | 6 ++-- .../app/portfolio/portfolio-calculator-new.ts | 34 +------------------ .../portfolio/portfolio-service.factory.ts | 19 ----------- .../portfolio/portfolio-service.strategy.ts | 25 ++++++++++++++ .../src/app/portfolio/portfolio.controller.ts | 27 ++++++++------- .../api/src/app/portfolio/portfolio.module.ts | 6 ++-- .../app/portfolio/portfolio.service-new.ts | 20 ++++++----- .../src/app/user/update-user-setting.dto.ts | 7 +++- apps/api/src/app/user/user.controller.ts | 8 ++++- .../pages/account/account-page.component.ts | 18 ++++++++++ .../src/app/pages/account/account-page.html | 17 ++++++++++ 12 files changed, 110 insertions(+), 81 deletions(-) delete mode 100644 apps/api/src/app/portfolio/portfolio-service.factory.ts create mode 100644 apps/api/src/app/portfolio/portfolio-service.strategy.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1414462a3..12dbd19fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a new calculation engine (experimental) + ### Fixed - Fixed the styling in the footer row of the activities table diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index e0049a904..b73977f75 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -1,4 +1,4 @@ -import { PortfolioServiceFactory } from '@ghostfolio/api/app/portfolio/portfolio-service.factory'; +import { PortfolioServiceStrategy } from '@ghostfolio/api/app/portfolio/portfolio-service.strategy'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { nullifyValuesInObject, @@ -35,7 +35,7 @@ export class AccountController { public constructor( private readonly accountService: AccountService, private readonly impersonationService: ImpersonationService, - private readonly portfolioServiceFactory: PortfolioServiceFactory, + private readonly portfolioServiceStrategy: PortfolioServiceStrategy, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @@ -91,7 +91,7 @@ export class AccountController { this.request.user.id ); - let accountsWithAggregations = await this.portfolioServiceFactory + let accountsWithAggregations = await this.portfolioServiceStrategy .get() .getAccountsWithAggregations(impersonationUserId || this.request.user.id); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 7f102795f..1f812a154 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -233,8 +233,6 @@ export class PortfolioCalculatorNew { const marketValue = marketSymbolMap[todayString]?.[item.symbol]; const { - // annualizedGrossPerformance, - // annualizedNetPerformance, grossPerformance, grossPerformancePercentage, hasErrors, @@ -340,7 +338,6 @@ export class PortfolioCalculatorNew { let lastAveragePrice = new Big(0); let lastValueOfInvestment = new Big(0); let lastNetValueOfInvestment = new Big(0); - let previousOrder: PortfolioOrder = null; let timeWeightedGrossPerformancePercentage = new Big(1); let timeWeightedNetPerformancePercentage = new Big(1); let totalInvestment = new Big(0); @@ -436,12 +433,6 @@ export class PortfolioCalculatorNew { .minus(totalInvestment) .plus(grossPerformanceFromSells); - const grossPerformanceSinceLastTransaction = - newGrossPerformance.minus(grossPerformance); - - const netPerformanceSinceLastTransaction = - grossPerformanceSinceLastTransaction.minus(previousOrder?.fee ?? 0); - if ( i > indexOfStartOrder && !lastValueOfInvestment @@ -491,31 +482,8 @@ export class PortfolioCalculatorNew { feesAtStartDate = fees; grossPerformanceAtStartDate = grossPerformance; } - - /*console.log(` - Symbol: ${symbol} - Date: ${order.date} - Price: ${order.unitPrice} - transactionInvestment: ${transactionInvestment} - totalUnits: ${totalUnits} - totalInvestment: ${totalInvestment} - valueOfInvestment: ${valueOfInvestment} - lastAveragePrice: ${lastAveragePrice} - grossPerformanceFromSell: ${grossPerformanceFromSell} - grossPerformanceFromSells: ${grossPerformanceFromSells} - grossPerformance: ${grossPerformance.minus(grossPerformanceAtStartDate)} - netPerformance: ${grossPerformance.minus(fees)} - netPerformanceSinceLastTransaction: ${netPerformanceSinceLastTransaction} - grossPerformanceSinceLastTransaction: ${grossPerformanceSinceLastTransaction} - timeWeightedGrossPerformancePercentage: ${timeWeightedGrossPerformancePercentage} - timeWeightedNetPerformancePercentage: ${timeWeightedNetPerformancePercentage} - `);*/ - - previousOrder = order; } - // console.log('\n---\n'); - timeWeightedGrossPerformancePercentage = timeWeightedGrossPerformancePercentage.sub(1); @@ -531,8 +499,8 @@ export class PortfolioCalculatorNew { .minus(fees.minus(feesAtStartDate)); return { - hasErrors: !initialValue || !unitPriceAtEndDate, initialValue, + hasErrors: !initialValue || !unitPriceAtEndDate, netPerformance: totalNetPerformance, netPerformancePercentage: timeWeightedNetPerformancePercentage, grossPerformance: totalGrossPerformance, diff --git a/apps/api/src/app/portfolio/portfolio-service.factory.ts b/apps/api/src/app/portfolio/portfolio-service.factory.ts deleted file mode 100644 index e5a3eaae5..000000000 --- a/apps/api/src/app/portfolio/portfolio-service.factory.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PortfolioService } from './portfolio.service'; -import { PortfolioServiceNew } from './portfolio.service-new'; - -@Injectable() -export class PortfolioServiceFactory { - public constructor( - private readonly portfolioService: PortfolioService, - private readonly portfolioServiceNew: PortfolioServiceNew - ) {} - - public get() { - if (false) { - return this.portfolioServiceNew; - } - - return this.portfolioService; - } -} diff --git a/apps/api/src/app/portfolio/portfolio-service.strategy.ts b/apps/api/src/app/portfolio/portfolio-service.strategy.ts new file mode 100644 index 000000000..49ec0422f --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-service.strategy.ts @@ -0,0 +1,25 @@ +import type { RequestWithUser } from '@ghostfolio/common/types'; +import { Inject, Injectable } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; + +import { PortfolioService } from './portfolio.service'; +import { PortfolioServiceNew } from './portfolio.service-new'; + +@Injectable() +export class PortfolioServiceStrategy { + public constructor( + private readonly portfolioService: PortfolioService, + private readonly portfolioServiceNew: PortfolioServiceNew, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} + + public get() { + if ( + this.request.user?.Settings?.settings?.['isNewCalculationEngine'] === true + ) { + return this.portfolioServiceNew; + } + + return this.portfolioService; + } +} diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 9aae2f772..a6d291ad2 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -35,7 +35,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; import { PortfolioPositions } from './interfaces/portfolio-positions.interface'; -import { PortfolioServiceFactory } from './portfolio-service.factory'; +import { PortfolioServiceStrategy } from './portfolio-service.strategy'; @Controller('portfolio') export class PortfolioController { @@ -43,7 +43,7 @@ export class PortfolioController { private readonly accessService: AccessService, private readonly configurationService: ConfigurationService, private readonly exchangeRateDataService: ExchangeRateDataService, - private readonly portfolioServiceFactory: PortfolioServiceFactory, + private readonly portfolioServiceStrategy: PortfolioServiceStrategy, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly userService: UserService ) {} @@ -55,7 +55,7 @@ export class PortfolioController { @Query('range') range, @Res() res: Response ): Promise { - const historicalDataContainer = await this.portfolioServiceFactory + const historicalDataContainer = await this.portfolioServiceStrategy .get() .getChart(impersonationId, range); @@ -114,9 +114,10 @@ export class PortfolioController { let hasError = false; - const { accounts, holdings, hasErrors } = await this.portfolioServiceFactory - .get() - .getDetails(impersonationId, this.request.user.id, range); + const { accounts, holdings, hasErrors } = + await this.portfolioServiceStrategy + .get() + .getDetails(impersonationId, this.request.user.id, range); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { hasError = true; @@ -174,7 +175,7 @@ export class PortfolioController { return res.json({}); } - let investments = await this.portfolioServiceFactory + let investments = await this.portfolioServiceStrategy .get() .getInvestments(impersonationId); @@ -203,7 +204,7 @@ export class PortfolioController { @Query('range') range, @Res() res: Response ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { - const performanceInformation = await this.portfolioServiceFactory + const performanceInformation = await this.portfolioServiceStrategy .get() .getPerformance(impersonationId, range); @@ -227,7 +228,7 @@ export class PortfolioController { @Query('range') range, @Res() res: Response ): Promise { - const result = await this.portfolioServiceFactory + const result = await this.portfolioServiceStrategy .get() .getPositions(impersonationId, range); @@ -268,7 +269,7 @@ export class PortfolioController { hasDetails = user.subscription.type === 'Premium'; } - const { holdings } = await this.portfolioServiceFactory + const { holdings } = await this.portfolioServiceStrategy .get() .getDetails(access.userId, access.userId); @@ -311,7 +312,7 @@ export class PortfolioController { public async getSummary( @Headers('impersonation-id') impersonationId ): Promise { - let summary = await this.portfolioServiceFactory + let summary = await this.portfolioServiceStrategy .get() .getSummary(impersonationId); @@ -342,7 +343,7 @@ export class PortfolioController { @Headers('impersonation-id') impersonationId: string, @Param('symbol') symbol ): Promise { - let position = await this.portfolioServiceFactory + let position = await this.portfolioServiceStrategy .get() .getPosition(impersonationId, symbol); @@ -386,7 +387,7 @@ export class PortfolioController { return ( res.json( - await this.portfolioServiceFactory.get().getReport(impersonationId) + await this.portfolioServiceStrategy.get().getReport(impersonationId) ) ); } diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index d7708a86d..85bc98e7a 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -13,14 +13,14 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.mod import { Module } from '@nestjs/common'; import { CurrentRateService } from './current-rate.service'; -import { PortfolioServiceFactory } from './portfolio-service.factory'; +import { PortfolioServiceStrategy } from './portfolio-service.strategy'; import { PortfolioController } from './portfolio.controller'; import { PortfolioService } from './portfolio.service'; import { PortfolioServiceNew } from './portfolio.service-new'; import { RulesService } from './rules.service'; @Module({ - exports: [PortfolioServiceFactory], + exports: [PortfolioServiceStrategy], imports: [ AccessModule, ConfigurationModule, @@ -40,7 +40,7 @@ import { RulesService } from './rules.service'; CurrentRateService, PortfolioService, PortfolioServiceNew, - PortfolioServiceFactory, + PortfolioServiceStrategy, RulesService ] }) diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 736385da0..da6bd44aa 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -399,11 +399,12 @@ export class PortfolioServiceNew { aImpersonationId: string, aSymbol: string ): Promise { + const userCurrency = this.request.user.Settings.currency; const userId = await this.getUserId(aImpersonationId, this.request.user.id); - const orders = (await this.orderService.getOrders({ userId })).filter( - (order) => order.symbol === aSymbol - ); + const orders = ( + await this.orderService.getOrders({ userCurrency, userId }) + ).filter((order) => order.symbol === aSymbol); if (orders.length <= 0) { return { @@ -871,24 +872,25 @@ export class PortfolioServiceNew { } public async getSummary(aImpersonationId: string): Promise { - const currency = this.request.user.Settings.currency; + const userCurrency = this.request.user.Settings.currency; const userId = await this.getUserId(aImpersonationId, this.request.user.id); const performanceInformation = await this.getPerformance(aImpersonationId); const { balance } = await this.accountService.getCashDetails( userId, - currency + userCurrency ); const orders = await this.orderService.getOrders({ + userCurrency, userId }); const dividend = this.getDividend(orders).toNumber(); const fees = this.getFees(orders).toNumber(); const firstOrderDate = orders[0]?.date; - const totalBuy = this.getTotalByType(orders, currency, 'BUY'); - const totalSell = this.getTotalByType(orders, currency, 'SELL'); + const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY'); + const totalSell = this.getTotalByType(orders, userCurrency, 'SELL'); const committedFunds = new Big(totalBuy).sub(totalSell); @@ -1051,8 +1053,11 @@ export class PortfolioServiceNew { orders: OrderWithAccount[]; portfolioOrders: PortfolioOrder[]; }> { + const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; + const orders = await this.orderService.getOrders({ includeDrafts, + userCurrency, userId, types: ['BUY', 'SELL'] }); @@ -1061,7 +1066,6 @@ export class PortfolioServiceNew { return { transactionPoints: [], orders: [], portfolioOrders: [] }; } - const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ currency: order.currency, dataSource: order.SymbolProfile?.dataSource ?? order.dataSource, diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index a6a583138..38dc9cafb 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,6 +1,11 @@ -import { IsBoolean } from 'class-validator'; +import { IsBoolean, IsOptional } from 'class-validator'; export class UpdateUserSettingDto { @IsBoolean() + @IsOptional() + isNewCalculationEngine?: boolean; + + @IsBoolean() + @IsOptional() isRestrictedView?: boolean; } diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 0a7ed21cf..adb059412 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -23,7 +23,7 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; -import { Provider, Role } from '@prisma/client'; +import { Provider } from '@prisma/client'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -115,6 +115,12 @@ export class UserController { ...data }; + for (const key in userSettings) { + if (userSettings[key] === false) { + delete userSettings[key]; + } + } + return await this.userService.updateUserSetting({ userSettings, userId: this.request.user.id diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index 5d1d0d5e3..df7f81beb 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -192,6 +192,24 @@ export class AccountPageComponent implements OnDestroy, OnInit { }); } + public onNewCalculationChange(aEvent: MatSlideToggleChange) { + this.dataService + .putUserSetting({ isNewCalculationEngine: aEvent.checked }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.userService.remove(); + + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); + }); + } + public onRedeemCoupon() { let couponCode = prompt('Please enter your coupon code:'); couponCode = couponCode?.trim(); diff --git a/apps/client/src/app/pages/account/account-page.html b/apps/client/src/app/pages/account/account-page.html index 1155741f9..081784bfb 100644 --- a/apps/client/src/app/pages/account/account-page.html +++ b/apps/client/src/app/pages/account/account-page.html @@ -135,6 +135,23 @@ > +
+
+
New Calculation Engine
+
Experimental
+
+
+ +
+