Feature/add feature toggle for new calculation engine (#649)
* Add feature toggle for new calculation engine * Update changelog
This commit is contained in:
parent
f15b33e950
commit
bcb7f5f522
@ -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;
|
||||
}
|
||||
}
|
25
apps/api/src/app/portfolio/portfolio-service.strategy.ts
Normal file
25
apps/api/src/app/portfolio/portfolio-service.strategy.ts
Normal file
@ -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…
x
Reference in New Issue
Block a user