diff --git a/CHANGELOG.md b/CHANGELOG.md index 78456527b6..8b00ee0b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Extended the cache key of the portfolio snapshot by the performance calculation type +- Hardened the validation of the device id in the biometric authentication + +### Fixed + +- Fixed the net performance percentage of date ranges in the portfolio performance calculation by including the gross performance at the start date ## 3.71.0 - 2026-09-19 diff --git a/README.md b/README.md index 5329e17a6b..c98c163c40 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony } ``` -### Import Activities +### Import Portfolio Data #### Prerequisites @@ -272,6 +272,22 @@ You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anony } ``` +### Export Portfolio Data + +#### Prerequisites + +[Bearer Token](#authorization-bearer-token) for authorization + +#### Request + +`GET http://localhost:3333/api/v1/export` + +#### Response + +##### Success + +`200 OK` + ### Portfolio (experimental) #### Prerequisites diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index da45f0071a..4cc42b5c16 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -28,6 +28,7 @@ import { Request, Response } from 'express'; import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { AuthService } from './auth.service'; +import { GenerateAuthenticationOptionsDto } from './generate-authentication-options.dto'; @AllowDuringImpersonation() @Controller('auth') @@ -122,7 +123,7 @@ export class AuthController { @Post('webauthn/generate-authentication-options') @UseGuards(CustomThrottlerGuard) public async generateAuthenticationOptions( - @Body() body: { deviceId: string } + @Body() body: GenerateAuthenticationOptionsDto ) { return this.webAuthService.generateAuthenticationOptions(body.deviceId); } diff --git a/apps/api/src/app/auth/generate-authentication-options.dto.ts b/apps/api/src/app/auth/generate-authentication-options.dto.ts new file mode 100644 index 0000000000..6cc45d9fc8 --- /dev/null +++ b/apps/api/src/app/auth/generate-authentication-options.dto.ts @@ -0,0 +1,6 @@ +import { IsUUID } from 'class-validator'; + +export class GenerateAuthenticationOptionsDto { + @IsUUID() + deviceId: string; +} diff --git a/apps/api/src/app/auth/web-auth.service.ts b/apps/api/src/app/auth/web-auth.service.ts index dadf5cdd51..40a86398a6 100644 --- a/apps/api/src/app/auth/web-auth.service.ts +++ b/apps/api/src/app/auth/web-auth.service.ts @@ -21,7 +21,8 @@ import { Inject, Injectable, InternalServerErrorException, - Logger + Logger, + NotFoundException } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; @@ -170,7 +171,7 @@ export class WebAuthService { const device = await this.deviceService.authDevice({ id: deviceId }); if (!device) { - throw new Error('Device not found'); + throw new NotFoundException('Device not found'); } // Compute in the background during the biometric authentication diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 5cd87e8d9a..d478076f7c 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -794,15 +794,25 @@ export abstract class PortfolioCalculator { const chart: HistoricalDataItem[] = []; + let grossPerformanceAtStartDate: number; + let grossPerformanceWithCurrencyEffectAtStartDate: number; let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; - const totalInvestmentValuesWithCurrencyEffect: number[] = []; + const timeWeightedInvestmentValues: number[] = []; + const timeWeightedInvestmentValuesWithCurrencyEffect: number[] = []; for (const historicalDataItem of historicalData) { const date = resetHours(parseDate(historicalDataItem.date)); if (!isBefore(date, start) && !isAfter(date, end)) { if (!isNumber(netPerformanceAtStartDate)) { + grossPerformanceAtStartDate = + historicalDataItem.value - historicalDataItem.totalInvestment; + + grossPerformanceWithCurrencyEffectAtStartDate = + historicalDataItem.valueWithCurrencyEffect - + historicalDataItem.totalInvestmentValueWithCurrencyEffect; + netPerformanceAtStartDate = historicalDataItem.netPerformance; netPerformanceWithCurrencyEffectAtStartDate = @@ -816,33 +826,48 @@ export abstract class PortfolioCalculator { historicalDataItem.netPerformanceWithCurrencyEffect - netPerformanceWithCurrencyEffectAtStartDate; + // Add the gross performance at the start date of the range to the + // investment of each day. Thus the range starts with the value of its + // first day, and subsequent buy and sell activities stay included. + if (historicalDataItem.totalInvestment > 0) { + timeWeightedInvestmentValues.push( + historicalDataItem.totalInvestment + grossPerformanceAtStartDate + ); + } + if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) { - totalInvestmentValuesWithCurrencyEffect.push( - historicalDataItem.totalInvestmentValueWithCurrencyEffect + timeWeightedInvestmentValuesWithCurrencyEffect.push( + historicalDataItem.totalInvestmentValueWithCurrencyEffect + + grossPerformanceWithCurrencyEffectAtStartDate ); } const timeWeightedInvestmentValue = - totalInvestmentValuesWithCurrencyEffect.length > 0 - ? sum(totalInvestmentValuesWithCurrencyEffect) / - totalInvestmentValuesWithCurrencyEffect.length + timeWeightedInvestmentValues.length > 0 + ? sum(timeWeightedInvestmentValues) / + timeWeightedInvestmentValues.length + : 0; + + const timeWeightedInvestmentValueWithCurrencyEffect = + timeWeightedInvestmentValuesWithCurrencyEffect.length > 0 + ? sum(timeWeightedInvestmentValuesWithCurrencyEffect) / + timeWeightedInvestmentValuesWithCurrencyEffect.length : 0; chart.push({ ...historicalDataItem, - netPerformance: - historicalDataItem.netPerformance - netPerformanceAtStartDate, + netPerformance: netPerformanceSinceStartDate, netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffectSinceStartDate, netPerformanceInPercentage: - timeWeightedInvestmentValue === 0 - ? 0 - : netPerformanceSinceStartDate / timeWeightedInvestmentValue, + timeWeightedInvestmentValue > 0 + ? netPerformanceSinceStartDate / timeWeightedInvestmentValue + : 0, netPerformanceInPercentageWithCurrencyEffect: - timeWeightedInvestmentValue === 0 - ? 0 - : netPerformanceWithCurrencyEffectSinceStartDate / - timeWeightedInvestmentValue + timeWeightedInvestmentValueWithCurrencyEffect > 0 + ? netPerformanceWithCurrencyEffectSinceStartDate / + timeWeightedInvestmentValueWithCurrencyEffect + : 0 }); } } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index e54fd96aa7..237c1ae220 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -190,10 +190,14 @@ describe('PortfolioCalculator', () => { netPerformance: new Big('27172.74').mul(0.97373), netPerformancePercentage: new Big('42.41978276196153750666'), netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('41.72313811883729606471') + '1d': new Big('-0.04016229506406263535'), + max: new Big('41.72313811883729606471'), + ytd: new Big('-0.04016229506406263535') }, netPerformanceWithCurrencyEffectMap: { - max: new Big('26516.208701400000064086') + '1d': new Big('-556.443324'), + max: new Big('26516.208701400000064086'), + ytd: new Big('-556.443324') }, quantity: new Big('1'), symbol: 'BTCUSD', @@ -281,8 +285,8 @@ describe('PortfolioCalculator', () => { '1d': { date: '2018-01-01', netPerformance: -486.0860160000011, - netPerformanceInPercentage: -1.5259683109961866, - netPerformanceInPercentageWithCurrencyEffect: -1.7468407881319952, + netPerformanceInPercentage: -0.035263202509112565, + netPerformanceInPercentageWithCurrencyEffect: -0.040162295064062624, netPerformanceWithCurrencyEffect: -556.4433239999998, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, valueWithCurrencyEffect: 13298.425356 @@ -290,7 +294,7 @@ describe('PortfolioCalculator', () => { max: { date: '2018-01-01', netPerformance: 26458.9121202, - netPerformanceInPercentage: 41.63298219956282, + netPerformanceInPercentage: 42.50435329547954, netPerformanceInPercentageWithCurrencyEffect: 41.72313811883715, netPerformanceWithCurrencyEffect: 26516.2087014, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, @@ -299,8 +303,8 @@ describe('PortfolioCalculator', () => { ytd: { date: '2018-01-01', netPerformance: -486.0860160000011, - netPerformanceInPercentage: -1.5259683109961866, - netPerformanceInPercentageWithCurrencyEffect: -1.7468407881319952, + netPerformanceInPercentage: -0.035263202509112565, + netPerformanceInPercentageWithCurrencyEffect: -0.040162295064062624, netPerformanceWithCurrencyEffect: -556.4433239999998, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, valueWithCurrencyEffect: 13298.425356 diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index bb16af5ef6..87e13e75c9 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -193,8 +193,8 @@ describe('PortfolioCalculator', () => { '1d': { date: '2023-07-10', netPerformance: -5.390000000000001, - netPerformanceInPercentage: -0.018052113336459244, - netPerformanceInPercentageWithCurrencyEffect: -0.018052113336459244, + netPerformanceInPercentage: -0.015983630864124312, + netPerformanceInPercentageWithCurrencyEffect: -0.015983630864124312, netPerformanceWithCurrencyEffect: -5.390000000000001, totalInvestmentValueWithCurrencyEffect: 298.58, valueWithCurrencyEffect: 331.83 @@ -211,8 +211,8 @@ describe('PortfolioCalculator', () => { ytd: { date: '2023-07-10', netPerformance: -7.68, - netPerformanceInPercentage: -0.025721749614843515, - netPerformanceInPercentageWithCurrencyEffect: -0.025721749614843515, + netPerformanceInPercentage: -0.022620835910577012, + netPerformanceInPercentageWithCurrencyEffect: -0.022620835910577012, netPerformanceWithCurrencyEffect: -7.68, totalInvestmentValueWithCurrencyEffect: 298.58, valueWithCurrencyEffect: 331.83