- {{ holding.assetProfile.name }}
+
+
+ {{ holding.assetProfile.name }}
+
Date: Fri, 9 Oct 2026 17:10:22 +0200
Subject: [PATCH 09/10] Bugfix/end date of calendar year date ranges in
portfolio performance (#8095)
* Fix end date of calendar year date ranges
* Update changelog
---
CHANGELOG.md | 1 +
.../calculator/portfolio-calculator.ts | 13 +++---
.../roai/portfolio-calculator.spec.ts | 44 +++++++++++++++++++
3 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d87e1535b..0a105dd492 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the missing list numbers in the top and bottom holdings on the analysis page in Safari
- Fixed the missing first data point in the portfolio evolution chart on the analysis page
+- Fixed the end date of calendar year date ranges in the portfolio performance calculation for instances in time zones with a negative UTC offset
- Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service
## 3.81.0 - 2026-10-07
diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
index 7a8bbcc741..ca440c6d2a 100644
--- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
+++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
@@ -1407,7 +1407,7 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalLiabilitiesWithCurrencyEffect;
}
- public async getPerformance({ end, start }) {
+ public async getPerformance({ end, start }: { end: Date; start: Date }) {
await this.snapshotPromise;
const { historicalData } = this.snapshot;
@@ -1418,14 +1418,13 @@ export abstract class PortfolioCalculator {
let netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: number;
- const startDate = isEqual(start, endOfDay(start))
- ? resetHours(start)
- : start;
+ const endDate = this.toChartDate(end);
+ const startDate = this.toChartDate(start);
for (const historicalDataItem of historicalData) {
const date = resetHours(parseDate(historicalDataItem.date));
- if (!isBefore(date, startDate) && !isAfter(date, end)) {
+ if (!isBefore(date, startDate) && !isAfter(date, endDate)) {
// Take the values at the start date from the first day of the date
// range
if (historicalDataItemsOfDateRange.length === 0) {
@@ -1915,4 +1914,8 @@ export abstract class PortfolioCalculator {
await this.initialize(attempt + 1);
}
}
+
+ private toChartDate(aDate: Date) {
+ return isEqual(aDate, endOfDay(aDate)) ? resetHours(aDate) : aDate;
+ }
}
diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
index 5f8d9cdc44..43ff160a78 100644
--- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
+++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
@@ -1,3 +1,7 @@
+/**
+ * @jest-environment /jest-environment-tz.js
+ * @jest-environment-options {"timeZone": "America/New_York"}
+ */
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@@ -9,6 +13,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
+import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@@ -183,4 +188,43 @@ describe('PortfolioCalculator', () => {
).toBeCloseTo(0.23, 10);
});
});
+
+ describe('get performance', () => {
+ it('with a calendar year date range', async () => {
+ const snapshot = await portfolioCalculator.getSnapshot();
+
+ snapshot.historicalData = [
+ getHistoricalDataItem({
+ date: '2016-12-31',
+ investment: 100,
+ netPerformance: 0
+ }),
+ getHistoricalDataItem({
+ date: '2017-12-31',
+ investment: 100,
+ netPerformance: 10
+ }),
+ getHistoricalDataItem({
+ date: '2018-01-01',
+ investment: 100,
+ netPerformance: 20
+ })
+ ];
+
+ const { endDate, startDate } = getIntervalFromDateRange({
+ dateRange: '2017'
+ });
+
+ const { chart } = await portfolioCalculator.getPerformance({
+ end: endDate,
+ start: startDate
+ });
+
+ expect(
+ chart.map(({ date }) => {
+ return date;
+ })
+ ).toEqual(['2016-12-31', '2017-12-31']);
+ });
+ });
});
From 03d69eb3bfee0bd9d9d8d5422fabd833eb31d80a Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 9 Oct 2026 17:11:55 +0200
Subject: [PATCH 10/10] Feature/add emoji flags to countries on product page
(#8097)
Add emoji flags to countries
---
.../interfaces/interfaces.ts | 12 +++++-
.../product-page.component.ts | 39 ++++++++++---------
.../personal-finance-tools/product-page.html | 32 ++++++++++++---
3 files changed, 58 insertions(+), 25 deletions(-)
diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts b/apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts
index 738719f0ae..c4cb55d23a 100644
--- a/apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts
+++ b/apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts
@@ -1,6 +1,16 @@
import type { Product } from '@ghostfolio/common/interfaces';
-export type ResolvedProduct = Omit & {
+export interface ResolvedRegion {
+ emojiFlag?: string;
+ name: string;
+}
+
+export type ResolvedProduct = Omit<
+ Product,
+ 'categories' | 'origin' | 'platforms' | 'regions'
+> & {
categories?: string[];
+ origin?: ResolvedRegion;
platforms?: string[];
+ regions?: ResolvedRegion[];
};
diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
index 773bfca92d..55aa86c7d5 100644
--- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
+++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
@@ -1,4 +1,4 @@
-import { getCountryName } from '@ghostfolio/common/helper';
+import { getCountryName, getEmojiFlag } from '@ghostfolio/common/helper';
import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { publicRoutes } from '@ghostfolio/common/routes/routes';
@@ -14,7 +14,7 @@ import {
import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router';
-import { ResolvedProduct } from './interfaces/interfaces';
+import { ResolvedProduct, ResolvedRegion } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -58,9 +58,9 @@ export class GfProductPageComponent {
// 'Ukrainian (Українська)'
],
name: 'Ghostfolio',
- origin: getCountryName({ code: 'CH' }),
+ origin: this.getResolvedCountry('CH'),
platforms: this.getSortedTranslations(['ANDROID', 'WEB']),
- regions: [$localize`Global`],
+ regions: [{ name: $localize`Global` }],
slogan: 'Open Source Wealth Management',
useAnonymously: true
}));
@@ -75,20 +75,16 @@ export class GfProductPageComponent {
name: product?.name ?? '',
...product,
categories: this.getSortedTranslations(product?.categories),
- platforms: this.getSortedTranslations(product?.platforms)
- };
-
- if (mappedProduct.origin) {
- mappedProduct.origin = getCountryName({ code: mappedProduct.origin });
- }
-
- if (mappedProduct.regions) {
- mappedProduct.regions = mappedProduct.regions.map((region) => {
+ origin: product?.origin
+ ? this.getResolvedCountry(product.origin)
+ : undefined,
+ platforms: this.getSortedTranslations(product?.platforms),
+ regions: product?.regions?.map((region) => {
return region === 'Global'
- ? translate(region)
- : getCountryName({ code: region });
- });
- }
+ ? { name: translate(region) }
+ : this.getResolvedCountry(region);
+ })
+ };
return mappedProduct;
});
@@ -122,7 +118,7 @@ export class GfProductPageComponent {
...(categories ?? []),
...(platforms ?? []),
name,
- origin
+ origin?.name
];
}
),
@@ -175,6 +171,13 @@ export class GfProductPageComponent {
});
}
+ private getResolvedCountry(code: string): ResolvedRegion {
+ return {
+ emojiFlag: getEmojiFlag(code),
+ name: getCountryName({ code })
+ };
+ }
+
private getSortedTranslations(values?: string[]) {
return values
?.map((value) => {
diff --git a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.html b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
index 01fb9a39d0..9c45e960db 100644
--- a/apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
+++ b/apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
@@ -100,27 +100,47 @@
| Origin |
- {{ product1().origin }} |
- {{ product2().origin }} |
+
+ @if (product1().origin; as origin) {
+ {{ origin.emojiFlag }} {{ origin.name }}
+ }
+ |
+
+ @if (product2().origin; as origin) {
+ {{ origin.emojiFlag }} {{ origin.name }}
+ }
+ |
| Region |
@for (
region of product1().regions;
- track region;
+ track region.name;
let isLast = $last
) {
- {{ region }}{{ isLast ? '' : ', ' }}
+ @if (region.emojiFlag) {
+ {{ region.emojiFlag }} {{ region.name }}{{ isLast ? '' : ', ' }}
+ } @else {
+ {{ region.name }}{{ isLast ? '' : ', ' }}
+ }
}
|
@for (
region of product2().regions;
- track region;
+ track region.name;
let isLast = $last
) {
- {{ region }}{{ isLast ? '' : ', ' }}
+ @if (region.emojiFlag) {
+ {{ region.emojiFlag }} {{ region.name }}{{ isLast ? '' : ', ' }}
+ } @else {
+ {{ region.name }}{{ isLast ? '' : ', ' }}
+ }
}
|