Feature/add feature toggle for new calculation engine (#649)

* Add feature toggle for new calculation engine

* Update changelog
pull/650/head
Thomas Kaul 2 years ago committed by GitHub
parent f15b33e950
commit bcb7f5f522
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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);

@ -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,

@ -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;
}
}

@ -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;
}
}

@ -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<PortfolioChart> {
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 <any>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<PortfolioPositions> {
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<PortfolioSummary> {
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<PortfolioPositionDetail> {
let position = await this.portfolioServiceFactory
let position = await this.portfolioServiceStrategy
.get()
.getPosition(impersonationId, symbol);
@ -386,7 +387,7 @@ export class PortfolioController {
return <any>(
res.json(
await this.portfolioServiceFactory.get().getReport(impersonationId)
await this.portfolioServiceStrategy.get().getReport(impersonationId)
)
);
}

@ -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
]
})

@ -399,11 +399,12 @@ export class PortfolioServiceNew {
aImpersonationId: string,
aSymbol: string
): Promise<PortfolioPositionDetail> {
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<PortfolioSummary> {
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,

@ -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;
}

@ -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

@ -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();

@ -135,6 +135,23 @@
></mat-slide-toggle>
</div>
</div>
<div
*ngIf="user?.subscription"
class="align-items-center d-flex mt-4 py-1"
>
<div class="pr-1 w-50">
<div i18n>New Calculation Engine</div>
<div class="hint-text text-muted" i18n>Experimental</div>
</div>
<div class="pl-1 w-50">
<mat-slide-toggle
color="primary"
[checked]="user.settings.isNewCalculationEngine"
[disabled]="!hasPermissionToUpdateUserSettings"
(change)="onNewCalculationChange($event)"
></mat-slide-toggle>
</div>
</div>
</mat-card-content>
</mat-card>
</div>

Loading…
Cancel
Save