From 214074a41fdb474d42145a29c362ba13ee83cee2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:21:43 +0200 Subject: [PATCH 1/3] Bugfix/date range performance percentage of portfolio (#7911) * Fix net performance percentage of date ranges * Update changelog --- CHANGELOG.md | 6 ++ .../calculator/portfolio-calculator.ts | 55 ++++++++++++++----- ...ator-btcusd-buy-and-sell-partially.spec.ts | 18 +++--- ...-calculator-msft-buy-with-dividend.spec.ts | 8 +-- 4 files changed, 61 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14bdfe11a4..0fb7e1e93f 100644 --- a/CHANGELOG.md +++ b/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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### 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 ### Added diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index dfbe99c8de..d73ea60161 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 From b96c18b0645be7135cd325f423a358b0508564d8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:26:01 +0200 Subject: [PATCH 2/3] Task/extend documentation with export API endpoint (#7894) Add export API documentation --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 From 6e128bba15e6170e0c8127dc75b458a4bb8b8570 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Sep 2026 19:31:23 +0200 Subject: [PATCH 3/3] Task/harden validation of device id in biometric authentication (#7915) * Fix internal server error caused by invalid device id in biometric authentication * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/auth/auth.controller.ts | 3 ++- .../api/src/app/auth/generate-authentication-options.dto.ts | 6 ++++++ apps/api/src/app/auth/web-auth.service.ts | 5 +++-- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/app/auth/generate-authentication-options.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb7e1e93f..6f75e481fe 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 +### Changed + +- 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 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 568822fb8d..6aa86a684c 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