From 7f7f99d1ea1fee7330feae2730fa34c5db69a6a4 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 28 Sep 2026 14:39:43 +0200
Subject: [PATCH 1/7] Task/extend personal finance tools (20260924) (#7944)
Extend personal finance tools
---
libs/common/src/lib/personal-finance-tools.ts | 437 +++++++++++++++++-
1 file changed, 436 insertions(+), 1 deletion(-)
diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts
index f713871dc8..9f891d3818 100644
--- a/libs/common/src/lib/personal-finance-tools.ts
+++ b/libs/common/src/lib/personal-finance-tools.ts
@@ -2,6 +2,21 @@ import { Product } from '@ghostfolio/common/interfaces';
export const personalFinanceTools: Product[] = (
[
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ key: '1fs-wealth',
+ name: '1fs Wealth',
+ origin: 'GB',
+ platforms: ['WEB'],
+ slogan: 'A single platform for all of your wealth.',
+ updatedAt: '2026-09-24',
+ url: 'https://www.1fs.co'
+ },
{
categories: [
'FAMILY_OFFICE',
@@ -44,7 +59,6 @@ export const personalFinanceTools: Product[] = (
url: 'https://addepar.com'
},
{
- alias: 'APX',
categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
hasSelfHostingAbility: true,
key: 'advent-portfolio-exchange',
@@ -77,6 +91,22 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-09',
url: 'https://akkuro.com'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ hasSelfHostingAbility: false,
+ key: 'aleta',
+ name: 'Aleta',
+ origin: 'US',
+ platforms: ['ANDROID', 'IOS', 'WEB'],
+ slogan: 'Family Office Software for Forward-Thinking Families',
+ updatedAt: '2026-09-24',
+ url: 'https://aleta.io'
+ },
{
categories: ['ETF_TRACKING', 'STOCK_TRACKING'],
founded: 2023,
@@ -174,6 +204,26 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://altruist.com'
},
+ {
+ categories: ['BUDGETING', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ hasFreePlan: true,
+ key: 'alwy',
+ languages: [
+ 'Dansk',
+ 'Deutsch',
+ 'English',
+ 'Nederlands',
+ 'Norsk',
+ 'Suomi',
+ 'Svenska'
+ ],
+ name: 'Alwy',
+ origin: 'SE',
+ platforms: ['IOS', 'WEB'],
+ slogan: 'Future-proof your business',
+ updatedAt: '2026-09-24',
+ url: 'https://alwy.se'
+ },
{
categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'],
founded: 2023,
@@ -215,6 +265,21 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://anlage.app'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'TAX_REPORTING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ key: 'arch',
+ name: 'Arch',
+ origin: 'US',
+ platforms: ['WEB'],
+ slogan: 'Private Markets. Simplified.',
+ updatedAt: '2026-09-24',
+ url: 'https://arch.com'
+ },
{
categories: ['FAMILY_OFFICE', 'WEALTH_MANAGEMENT'],
founded: 2002,
@@ -240,6 +305,39 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-09-23',
url: 'https://asignal.io'
},
+ {
+ categories: ['FAMILY_OFFICE', 'NET_WORTH_TRACKING', 'WEALTH_MANAGEMENT'],
+ founded: 2021,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'asora',
+ name: 'Asora',
+ origin: 'IE',
+ platforms: ['ANDROID', 'IOS', 'WEB'],
+ pricingPerYear: '$10800',
+ slogan: 'Software for the modern family office',
+ updatedAt: '2026-09-24',
+ url: 'https://www.asora.com'
+ },
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ founded: 2011,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'asset-vantage',
+ name: 'Asset Vantage',
+ platforms: ['ANDROID', 'IOS', 'WEB'],
+ pricingPerYear: '$30000',
+ slogan:
+ 'The Financial Operating System for Modern Family Offices and Their Advisors',
+ updatedAt: '2026-09-24',
+ url: 'https://www.assetvantage.com'
+ },
{
categories: ['FAMILY_OFFICE'],
founded: 2022,
@@ -253,6 +351,33 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.asseta.ai'
},
+ {
+ categories: ['FAMILY_OFFICE', 'WEALTH_MANAGEMENT'],
+ key: 'atalaya',
+ languages: ['English', 'Español'],
+ name: 'Atalaya',
+ origin: 'ES',
+ slogan: 'Software for Family Offices',
+ updatedAt: '2026-09-24',
+ url: 'https://www.atalaya.ai'
+ },
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'TAX_REPORTING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ hasSelfHostingAbility: false,
+ key: 'atlasfive',
+ languages: ['English'],
+ name: 'AtlasFive®',
+ origin: 'US',
+ slogan: 'Experience Wealth Intelligence',
+ updatedAt: '2026-09-24',
+ url: 'https://eton-solutions.com'
+ },
{
categories: ['WEALTH_MANAGEMENT'],
founded: 2016,
@@ -263,6 +388,20 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.atominvest.co'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ key: 'auria',
+ name: 'Auria',
+ platforms: ['WEB'],
+ slogan: 'Simplify the complexities of wealth with a unified solution',
+ updatedAt: '2026-09-24',
+ url: 'https://auriaplatform.com'
+ },
{
categories: ['BUDGETING'],
founded: 2020,
@@ -506,6 +645,41 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-09',
url: 'https://www.buxfer.com'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ founded: 2017,
+ hasSelfHostingAbility: false,
+ key: 'canopy',
+ languages: [
+ 'Bahasa Indonesia',
+ 'Bahasa Melayu',
+ 'Deutsch',
+ 'English',
+ 'Español',
+ 'Français',
+ 'Italiano',
+ 'Português',
+ 'Tiếng Việt',
+ 'Русский',
+ 'العربية',
+ 'ภาษาไทย',
+ '日本語',
+ '简体中文',
+ '繁體中文',
+ '한국어'
+ ],
+ name: 'Canopy',
+ origin: 'SG',
+ platforms: ['ANDROID', 'IOS', 'WEB'],
+ slogan: 'All your wealth, finally understood.',
+ updatedAt: '2026-09-24',
+ url: 'https://www.canopy.cloud'
+ },
{
categories: ['NET_WORTH_TRACKING'],
hasFreePlan: true,
@@ -557,6 +731,19 @@ export const personalFinanceTools: Product[] = (
slogan: 'Toma el control total de tus finanzas.',
updatedAt: '2025-07-27'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ founded: 2016,
+ hasFreePlan: false,
+ key: 'clockwork',
+ name: 'Clockwork',
+ origin: 'US',
+ platforms: ['WEB'],
+ slogan:
+ 'Manage your alternative investments, and everything else, with Clockwork.',
+ updatedAt: '2026-09-24',
+ url: 'https://www.clockwork.app'
+ },
{
categories: ['WEALTH_MANAGEMENT'],
founded: 2011,
@@ -882,6 +1069,22 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-08-16',
url: 'https://www.donkycapital.com'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'NET_WORTH_TRACKING',
+ 'TAX_REPORTING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ founded: 2006,
+ key: 'elysys',
+ name: 'Elysys',
+ origin: 'MC',
+ slogan:
+ 'Complete front-to-back investment accounting and portfolio management on Business Central',
+ updatedAt: '2026-09-24',
+ url: 'https://www.elysys.com'
+ },
{
categories: ['BUDGETING', 'FINANCIAL_ADVISOR', 'FINANCIAL_PLANNING'],
hasSelfHostingAbility: false,
@@ -1116,6 +1319,17 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-08-16',
url: 'https://extraetf.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ hasSelfHostingAbility: false,
+ key: 'fa-platform',
+ languages: ['English'],
+ name: 'FA Platform',
+ origin: 'FI',
+ slogan: 'Empowering Investment Management with Innovation',
+ updatedAt: '2026-09-24',
+ url: 'https://fasolutions.com'
+ },
{
categories: ['INVESTMENT_RESEARCH'],
key: 'factorstoday',
@@ -1442,6 +1656,20 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.fundmanagersoftware.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'TAX_REPORTING', 'WEALTH_MANAGEMENT'],
+ founded: 1999,
+ hasFreePlan: false,
+ hasSelfHostingAbility: true,
+ key: 'fundcount',
+ name: 'FundCount',
+ origin: 'BB',
+ platforms: ['LINUX', 'MACOS', 'WEB', 'WINDOWS'],
+ pricingPerYear: '$14450',
+ slogan: 'One source of truth for every entity, fund, and asset class.',
+ updatedAt: '2026-09-24',
+ url: 'https://fundcount.com'
+ },
{
categories: ['BUDGETING'],
hasFreePlan: true,
@@ -1626,6 +1854,16 @@ export const personalFinanceTools: Product[] = (
pricingPerYear: '$120',
updatedAt: '2025-08-27'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ key: 'infront',
+ name: 'Infront',
+ origin: 'NO',
+ platforms: ['WEB'],
+ slogan: 'Built for Complex Wealth. Made for Family Offices',
+ updatedAt: '2026-09-24',
+ url: 'https://www.infront.co/global/en/your-industry/wealth/family-offices.html'
+ },
{
categories: ['INVESTMENT_RESEARCH'],
hasFreePlan: true,
@@ -1760,6 +1998,18 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-09-23',
url: 'https://istfin.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ hasSelfHostingAbility: false,
+ key: 'j-ray',
+ languages: ['English'],
+ name: 'J-Ray',
+ origin: 'FI',
+ platforms: ['WEB'],
+ slogan: 'The Guiding Light for Complex Wealth',
+ updatedAt: '2026-09-24',
+ url: 'https://jaysolutions.com'
+ },
{
categories: ['ETF_TRACKING'],
founded: 2011,
@@ -1774,6 +2024,18 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.justetf.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ founded: 2004,
+ hasSelfHostingAbility: true,
+ key: 'keesense',
+ languages: ['English', 'Français'],
+ name: 'KeeSense',
+ origin: 'MC',
+ slogan: 'Augmented Wealth Management',
+ updatedAt: '2026-09-24',
+ url: 'https://www.keesystem.com'
+ },
{
categories: ['CRYPTOCURRENCY', 'TAX_REPORTING'],
founded: 2018,
@@ -2233,6 +2495,23 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://myfinancetools.io'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'NET_WORTH_TRACKING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ hasSelfHostingAbility: false,
+ key: 'myfo',
+ languages: ['English'],
+ name: 'MYFO',
+ origin: 'CA',
+ platforms: ['WEB'],
+ slogan: 'The First Full Family Office Operating System',
+ updatedAt: '2026-09-24',
+ url: 'https://www.myfotech.com'
+ },
{
categories: ['CRYPTOCURRENCY', 'NET_WORTH_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
@@ -2499,6 +2778,19 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.getpennies.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ founded: 2020,
+ hasSelfHostingAbility: false,
+ key: 'performativ',
+ languages: ['Deutsch', 'English', 'Nederlands'],
+ name: 'Performativ',
+ origin: 'DK',
+ platforms: ['WEB'],
+ slogan: 'The Operating System Powering Tomorrow’s Wealth Management',
+ updatedAt: '2026-09-24',
+ url: 'https://www.performativ.com'
+ },
{
categories: ['FAMILY_OFFICE'],
hasFreePlan: true,
@@ -2552,6 +2844,22 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.plannix.co'
},
+ {
+ categories: [
+ 'CRYPTOCURRENCY',
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'WEALTH_MANAGEMENT'
+ ],
+ founded: 2012,
+ hasSelfHostingAbility: true,
+ key: 'pmplus',
+ name: 'PMplus®',
+ origin: 'CH',
+ platforms: ['WEB'],
+ updatedAt: '2026-09-24',
+ url: 'https://www.suissetechpartners.com'
+ },
{
categories: ['BUDGETING'],
founded: 2015,
@@ -2582,6 +2890,17 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.pocketsmith.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ founded: 2017,
+ key: 'point',
+ languages: ['English'],
+ name: 'Point',
+ origin: 'GB',
+ slogan: 'AI-Ready Investment Data',
+ updatedAt: '2026-09-24',
+ url: 'https://pointgroup.io'
+ },
{
categories: ['FINANCIAL_PLANNING', 'WEALTH_MANAGEMENT'],
hasFreePlan: false,
@@ -2775,6 +3094,16 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.portseido.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ key: 'private-wealth-systems',
+ name: 'Private Wealth Systems',
+ origin: 'US',
+ platforms: ['WEB'],
+ slogan: 'Portfolio Management & Reporting Software',
+ updatedAt: '2026-09-24',
+ url: 'https://privatewealthsystems.com'
+ },
{
categories: ['FINANCIAL_PLANNING'],
founded: 2021,
@@ -2815,6 +3144,23 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-09-23',
url: 'https://www.assetbook.com'
},
+ {
+ categories: [
+ 'FAMILY_OFFICE',
+ 'FINANCIAL_ADVISOR',
+ 'TAX_REPORTING',
+ 'WEALTH_MANAGEMENT'
+ ],
+ founded: 2012,
+ key: 'qplix',
+ languages: ['Deutsch', 'English'],
+ name: 'QPLIX',
+ origin: 'DE',
+ platforms: ['IOS', 'WEB'],
+ slogan: 'Your Software to Master Wealth Management.',
+ updatedAt: '2026-09-24',
+ url: 'https://www.qplix.com'
+ },
{
categories: ['NET_WORTH_TRACKING'],
hasSelfHostingAbility: false,
@@ -2963,6 +3309,20 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.segmio.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ founded: 2019,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'sesame-one',
+ name: 'Sesame One',
+ origin: 'GB',
+ platforms: ['WEB'],
+ pricingPerYear: '£20000',
+ slogan: 'Spend more time building investment value, not spreadsheets',
+ updatedAt: '2026-09-24',
+ url: 'https://www.landytech.com'
+ },
{
categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'],
key: 'sharescope',
@@ -3360,6 +3720,30 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.sumio.app'
},
+ {
+ categories: ['FAMILY_OFFICE'],
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'sumit',
+ languages: ['English'],
+ name: 'SumIt',
+ platforms: ['WEB'],
+ slogan: 'Powerfully simple family office accounting.',
+ updatedAt: '2026-09-24',
+ url: 'https://getsumit.com'
+ },
+ {
+ categories: ['FAMILY_OFFICE', 'WEALTH_MANAGEMENT'],
+ hasSelfHostingAbility: true,
+ key: 'swimbird',
+ languages: ['English'],
+ name: 'Swimbird',
+ origin: 'SE',
+ platforms: ['ANDROID', 'IOS'],
+ slogan: 'Wealth Management Software',
+ updatedAt: '2026-09-24',
+ url: 'https://www.swimbird.com'
+ },
{
categories: ['INVESTMENT_RESEARCH'],
key: 'tc2000',
@@ -3408,6 +3792,22 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://tiller.com'
},
+ {
+ categories: [
+ 'FINANCIAL_ADVISOR',
+ 'INVESTMENT_RESEARCH',
+ 'WEALTH_MANAGEMENT'
+ ],
+ founded: 2010,
+ hasFreePlan: false,
+ key: 'tindeco',
+ languages: ['English'],
+ name: 'Tindeco',
+ origin: 'CH',
+ slogan: 'Scalable Investing Made Simple',
+ updatedAt: '2026-09-24',
+ url: 'https://tindecofs.com'
+ },
{
categories: ['CRYPTOCURRENCY', 'DIVIDEND_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
@@ -3858,6 +4258,17 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.wallstreetzen.com'
},
+ {
+ categories: ['FINANCIAL_ADVISOR', 'FINANCIAL_PLANNING'],
+ key: 'wealth-wizards',
+ languages: ['English'],
+ name: 'Wealth Wizards',
+ origin: 'GB',
+ regions: ['GB'],
+ slogan: 'Get new regime ready.',
+ updatedAt: '2026-09-24',
+ url: 'https://www.wealthwizards.com'
+ },
{
categories: ['FAMILY_OFFICE', 'TAX_REPORTING'],
founded: 2001,
@@ -3940,6 +4351,18 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-07-05',
url: 'https://www.wealthposition.com'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ hasSelfHostingAbility: true,
+ key: 'wealthspectrum',
+ languages: ['English'],
+ name: 'WealthSpectrum',
+ origin: 'IN',
+ platforms: ['WEB'],
+ slogan: 'Powering smart investment operations',
+ updatedAt: '2026-09-24',
+ url: 'https://www.wealthspectrum.com'
+ },
{
categories: ['STOCK_TRACKING'],
founded: 2018,
@@ -3970,6 +4393,18 @@ export const personalFinanceTools: Product[] = (
updatedAt: '2026-08-28',
url: 'https://www.wisesheets.io'
},
+ {
+ categories: ['FAMILY_OFFICE', 'FINANCIAL_ADVISOR', 'WEALTH_MANAGEMENT'],
+ founded: 2010,
+ hasSelfHostingAbility: true,
+ key: 'wize',
+ languages: ['English'],
+ name: 'WIZE',
+ origin: 'CH',
+ slogan: 'The all-in-one Wealth & Asset management IT platform',
+ updatedAt: '2026-09-24',
+ url: 'https://www.wize.net'
+ },
{
categories: [
'FINANCIAL_ADVISOR',
From cf0bdba6eb519b9ff96028a6299a0dc774ecb7f7 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 28 Sep 2026 14:40:08 +0200
Subject: [PATCH 2/7] Task/upgrade chartjs-chart-treemap to version 4.2.2
(#7959)
* Update chartjs-chart-treemap to version 4.2.2
* Update changelog
---
CHANGELOG.md | 6 ++++++
package-lock.json | 8 ++++----
package.json | 2 +-
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6e705c788e..3f72952ccc 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
+
+### Changed
+
+- Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2`
+
## 3.74.0 - 2026-09-27
### Added
diff --git a/package-lock.json b/package-lock.json
index 62a865e1b3..0bccf99a2b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -59,7 +59,7 @@
"bull": "4.16.5",
"chart.js": "4.5.1",
"chartjs-adapter-date-fns": "3.0.0",
- "chartjs-chart-treemap": "4.2.0",
+ "chartjs-chart-treemap": "4.2.2",
"chartjs-plugin-annotation": "3.1.0",
"chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.2.0",
@@ -18249,9 +18249,9 @@
}
},
"node_modules/chartjs-chart-treemap": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/chartjs-chart-treemap/-/chartjs-chart-treemap-4.2.0.tgz",
- "integrity": "sha512-2ghdKHbknLYEzqD2fRTbACOzEhuH/f7zI5t6dbQ3cAOS+ric4mLjll6Sxsaugy/HFU0KUftJEszYOvtay3wFSQ==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/chartjs-chart-treemap/-/chartjs-chart-treemap-4.2.2.tgz",
+ "integrity": "sha512-gmOVr5bokdhs0RsHqrnj1oQu9ybaRjdlXubzLhmRB9DsRnpY51WmyqxUd8JCCKbnFVru1yjdeOTXyqeMIFhxlg==",
"license": "MIT",
"peerDependencies": {
"chart.js": ">=3.0.0"
diff --git a/package.json b/package.json
index 91907a9f18..bb1c66a955 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
"bull": "4.16.5",
"chart.js": "4.5.1",
"chartjs-adapter-date-fns": "3.0.0",
- "chartjs-chart-treemap": "4.2.0",
+ "chartjs-chart-treemap": "4.2.2",
"chartjs-plugin-annotation": "3.1.0",
"chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.2.0",
From ca47dbfa40f46e037da1704e0511d0364acdfdcd Mon Sep 17 00:00:00 2001
From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com>
Date: Mon, 28 Sep 2026 19:47:20 +0700
Subject: [PATCH 3/7] Task/widen input types on value component (#7953)
Widen input types
---
libs/ui/src/lib/value/value.component.html | 2 +-
libs/ui/src/lib/value/value.component.ts | 13 ++++++++-----
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html
index 3ab711f7e6..c5ebe4e2cf 100644
--- a/libs/ui/src/lib/value/value.component.html
+++ b/libs/ui/src/lib/value/value.component.html
@@ -46,7 +46,7 @@
[class.justify-content-end]="position === 'end'"
>
@if (isNumber || value === null) {
- @if (colorizeSign && !useAbsoluteValue) {
+ @if (colorizeSign && !useAbsoluteValue && value !== null) {
@if (+value > 0) {
+
}
diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts
index cc08e5a248..c2c7d4621d 100644
--- a/libs/ui/src/lib/value/value.component.ts
+++ b/libs/ui/src/lib/value/value.component.ts
@@ -42,12 +42,12 @@ export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() isCurrency = false;
@Input() isDate = false;
@Input() isPercent = false;
- @Input() locale: string;
+ @Input() locale?: string;
@Input() position = '';
@Input() size: 'large' | 'medium' | 'small' = 'small';
- @Input() subLabel = '';
- @Input() unit = '';
- @Input() value: number | string = '';
+ @Input() subLabel?: string = '';
+ @Input() unit?: string | null = '';
+ @Input() value?: number | string | null = '';
@ViewChild('labelContent', { static: false })
labelContent!: ElementRef;
@@ -219,11 +219,14 @@ export class GfValueComponent implements AfterViewInit, OnChanges, OnDestroy {
this.isCopied = false;
this.isNumber = false;
this.isString = false;
- this.locale = this.locale || getLocale();
this.useAbsoluteValue = false;
if (this.copyToClipboardTimeout) {
clearTimeout(this.copyToClipboardTimeout);
}
+
+ if (this.locale === undefined || this.locale === '') {
+ this.locale = getLocale();
+ }
}
}
From 23c6452a8298b42acadbbd00908549bddeadf66f Mon Sep 17 00:00:00 2001
From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com>
Date: Mon, 28 Sep 2026 19:57:32 +0700
Subject: [PATCH 4/7] Task/widen input types in portfolio proportion chart
component (#7954)
Widen input types
---
.../portfolio-proportion-chart.component.ts | 28 +++++++++++--------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
index 9a215ee5b3..bf2f595c26 100644
--- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
+++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
@@ -1,5 +1,5 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
-import { UNKNOWN_KEY } from '@ghostfolio/common/config';
+import { DEFAULT_COLOR_SCHEME, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale, getSum, getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types';
@@ -60,20 +60,20 @@ const {
export class GfPortfolioProportionChartComponent
implements AfterViewInit, OnChanges, OnDestroy
{
- @Input() baseCurrency: string;
- @Input() colorScheme: ColorScheme;
+ @Input() baseCurrency?: string;
+ @Input() colorScheme?: ColorScheme;
@Input() cursor: string;
@Input() data: {
[symbol: string]: Pick & {
dataSource?: DataSource;
isClickable?: boolean;
- name: string;
+ name?: string | null;
value: number;
};
} = {};
- @Input() isInPercentage = false;
+ @Input() isInPercentage?: boolean = false;
@Input() keys: string[] = [];
- @Input() locale = getLocale();
+ @Input() locale?: string = getLocale();
@Input() maxItems?: number;
@Input() showLabels = false;
@@ -114,6 +114,7 @@ export class GfPortfolioProportionChartComponent
private initialize() {
this.isLoading = true;
+
const chartData: {
[symbol: string]: {
color?: string;
@@ -122,9 +123,12 @@ export class GfPortfolioProportionChartComponent
value: Big;
};
} = {};
+
+ const textColor = getTextColor(this.colorScheme ?? DEFAULT_COLOR_SCHEME);
+
this.colorMap = {
- [this.OTHER_KEY]: `rgba(${getTextColor(this.colorScheme)}, 0.24)`,
- [UNKNOWN_KEY]: `rgba(${getTextColor(this.colorScheme)}, 0.12)`
+ [this.OTHER_KEY]: `rgba(${textColor}, 0.24)`,
+ [UNKNOWN_KEY]: `rgba(${textColor}, 0.12)`
};
if (this.keys.length > 0) {
@@ -178,7 +182,7 @@ export class GfPortfolioProportionChartComponent
);
} else {
chartData[UNKNOWN_KEY] = {
- name: this.data[symbol].name,
+ name: this.data[symbol].name ?? '',
subCategory: secondaryKey
? { [secondaryKey]: { value: new Big(0) } }
: undefined,
@@ -190,7 +194,7 @@ export class GfPortfolioProportionChartComponent
} else {
Object.keys(this.data).forEach((symbol) => {
chartData[symbol] = {
- name: this.data[symbol].name,
+ name: this.data[symbol].name ?? '',
value: new Big(this.data[symbol].value || 0)
};
});
@@ -444,7 +448,7 @@ export class GfPortfolioProportionChartComponent
): Partial> {
return {
...getTooltipOptions({
- colorScheme: this.colorScheme,
+ colorScheme: this.colorScheme ?? DEFAULT_COLOR_SCHEME,
currency: this.baseCurrency,
locale: this.locale
}),
@@ -486,7 +490,7 @@ export class GfPortfolioProportionChartComponent
`${value.toLocaleString(this.locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
- })} ${this.baseCurrency} (${percentage.toFixed(2)}%)`
+ })} ${this.baseCurrency ?? ''} (${percentage.toFixed(2)}%)`
];
}
},
From 9134e7f2a82f1c00f1802cae6a8c7674f4a24696 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 28 Sep 2026 15:10:44 +0200
Subject: [PATCH 5/7] Bugfix/fallback to latest market price in current rate
service (#7948)
* Fix fallback to latest market price in current rate service
* Update changelog
---
CHANGELOG.md | 5 +
.../portfolio/current-rate.service.spec.ts | 96 ++++++++++++++++++-
.../src/app/portfolio/current-rate.service.ts | 27 ++++--
.../app/portfolio/portfolio.service.spec.ts | 59 ++++++++----
.../src/app/portfolio/portfolio.service.ts | 32 +++++--
.../manual/manual.service.spec.ts | 96 +++++++++++++++++++
.../data-provider/manual/manual.service.ts | 29 +++---
7 files changed, 294 insertions(+), 50 deletions(-)
create mode 100644 apps/api/src/services/data-provider/manual/manual.service.spec.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f72952ccc..a03fdc09a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Improved the get quotes functionality of the _Manual_ service
- Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2`
+### Fixed
+
+- Fixed an issue where holdings without a quote have been valued at the unit price of the latest activity instead of the latest market price
+
## 3.74.0 - 2026-09-27
### Added
diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts
index 4bb3a1b8cf..b2ec6cd2d8 100644
--- a/apps/api/src/app/portfolio/current-rate.service.spec.ts
+++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts
@@ -1,9 +1,12 @@
+import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
+import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
-import { DataSource, MarketData } from '@prisma/client';
+import { DataSource, MarketData, Order } from '@prisma/client';
+import { addDays, subDays } from 'date-fns';
import { CurrentRateService } from './current-rate.service';
import { DateQuery } from './interfaces/date-query.interface';
@@ -25,6 +28,9 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
state: 'CLOSE'
});
},
+ getLatest: () => {
+ return Promise.resolve(null);
+ },
getRange: ({
assetProfileIdentifiers,
dateQuery
@@ -97,12 +103,19 @@ jest.mock('@ghostfolio/api/services/property/property.service', () => {
});
describe('CurrentRateService', () => {
+ let activitiesService: ActivitiesService;
let currentRateService: CurrentRateService;
let dataProviderService: DataProviderService;
let marketDataService: MarketDataService;
let propertyService: PropertyService;
beforeAll(async () => {
+ activitiesService = {
+ getLatestActivity: () => {
+ return Promise.resolve(null);
+ }
+ } as unknown as ActivitiesService;
+
propertyService = new PropertyService(null);
dataProviderService = new DataProviderService(
@@ -117,7 +130,7 @@ describe('CurrentRateService', () => {
marketDataService = new MarketDataService(null);
currentRateService = new CurrentRateService(
- null,
+ activitiesService,
dataProviderService,
marketDataService
);
@@ -151,4 +164,83 @@ describe('CurrentRateService', () => {
]
});
});
+
+ describe('getValues without a quote', () => {
+ const today = resetHours(new Date());
+ const yesterday = subDays(today, 1);
+
+ const dataGatheringItems: AssetProfileIdentifier[] = [
+ { dataSource: DataSource.YAHOO, symbol: 'AMZN' }
+ ];
+
+ const dateQuery: DateQuery = {
+ gte: yesterday,
+ lt: addDays(today, 1)
+ };
+
+ beforeEach(() => {
+ jest.spyOn(dataProviderService, 'getQuotes').mockResolvedValue({});
+ jest.spyOn(marketDataService, 'getRangeCount').mockResolvedValue(0);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should fall back to the latest market price', async () => {
+ const getLatestActivity = jest
+ .spyOn(activitiesService, 'getLatestActivity')
+ .mockResolvedValue({ unitPrice: 1000 } as Order);
+
+ jest.spyOn(marketDataService, 'getLatest').mockResolvedValue({
+ createdAt: yesterday,
+ dataSource: DataSource.YAHOO,
+ date: yesterday,
+ id: '4f2b8a1e-7c3d-4e5f-9a6b-1c2d3e4f5a6b',
+ isCarriedForward: false,
+ marketPrice: 1847.839966,
+ state: 'CLOSE',
+ symbol: 'AMZN'
+ });
+
+ const { errors, values } = await currentRateService.getValues({
+ dataGatheringItems,
+ dateQuery
+ });
+
+ expect(getLatestActivity).not.toHaveBeenCalled();
+ expect(errors).toEqual(dataGatheringItems);
+ expect(values).toEqual([
+ {
+ dataSource: DataSource.YAHOO,
+ date: today,
+ marketPrice: 1847.839966,
+ symbol: 'AMZN'
+ }
+ ]);
+ });
+
+ it('should fall back to the unit price of the latest activity without market data', async () => {
+ jest
+ .spyOn(activitiesService, 'getLatestActivity')
+ .mockResolvedValue({ unitPrice: 1000 } as Order);
+
+ jest.spyOn(marketDataService, 'getLatest').mockResolvedValue(null);
+
+ const { errors, values } = await currentRateService.getValues({
+ dataGatheringItems,
+ dateQuery
+ });
+
+ expect(errors).toEqual(dataGatheringItems);
+ expect(values).toEqual([
+ {
+ dataSource: DataSource.YAHOO,
+ date: today,
+ marketPrice: 1000,
+ symbol: 'AMZN'
+ }
+ ]);
+ });
+ });
});
diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts
index 66119a3c63..79363c6344 100644
--- a/apps/api/src/app/portfolio/current-rate.service.ts
+++ b/apps/api/src/app/portfolio/current-rate.service.ts
@@ -132,18 +132,29 @@ export class CurrentRateService {
});
if (!value) {
- // Fallback to unit price of latest activity
- const latestActivity =
- await this.activitiesService.getLatestActivity({
- dataSource,
- symbol
- });
+ const latestMarketData = await this.marketDataService.getLatest({
+ dataSource,
+ symbol
+ });
+
+ let marketPrice = latestMarketData?.marketPrice;
+
+ if (!marketPrice) {
+ // Fallback to unit price of latest activity
+ const latestActivity =
+ await this.activitiesService.getLatestActivity({
+ dataSource,
+ symbol
+ });
+
+ marketPrice = latestActivity?.unitPrice ?? 0;
+ }
value = {
dataSource,
+ marketPrice,
symbol,
- date: today,
- marketPrice: latestActivity?.unitPrice ?? 0
+ date: today
};
response.values.push(value);
diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts
index 223a8964c1..ccd26e0345 100644
--- a/apps/api/src/app/portfolio/portfolio.service.spec.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts
@@ -4,6 +4,7 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.ser
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';
+import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
@@ -33,6 +34,7 @@ describe('PortfolioService', () => {
let accountService: AccountService;
let activitiesService: ActivitiesService;
let configurationService: ConfigurationService;
+ let currentRateService: CurrentRateService;
let dataProviderService: DataProviderService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
@@ -82,6 +84,12 @@ describe('PortfolioService', () => {
null
);
+ currentRateService = new CurrentRateService(
+ activitiesService,
+ dataProviderService,
+ null
+ );
+
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
null,
@@ -110,6 +118,7 @@ describe('PortfolioService', () => {
activitiesService,
null,
portfolioCalculatorFactory,
+ currentRateService,
dataProviderService,
exchangeRateDataService,
null,
@@ -901,15 +910,19 @@ describe('PortfolioService', () => {
count: 4
});
- const getQuotes = jest
- .spyOn(dataProviderService, 'getQuotes')
+ const getValues = jest
+ .spyOn(currentRateService, 'getValues')
.mockResolvedValue({
- 'YAHOO-AAPL': {
- currency: 'CHF',
- dataSource: DataSource.YAHOO,
- marketPrice: 200,
- marketState: 'open'
- }
+ dataProviderInfos: [],
+ errors: [],
+ values: [
+ {
+ dataSource: DataSource.YAHOO,
+ date: new Date(),
+ marketPrice: 200,
+ symbol: 'AAPL'
+ }
+ ]
});
const summary = await getSummary({
@@ -921,10 +934,10 @@ describe('PortfolioService', () => {
userId: userDummyData.id
});
- // The closed holding (MSFT) does not need a quote
- expect(getQuotes).toHaveBeenCalledWith(
+ // The closed holding (MSFT) does not need a market price
+ expect(getValues).toHaveBeenCalledWith(
expect.objectContaining({
- items: [{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }]
+ dataGatheringItems: [{ dataSource: DataSource.YAHOO, symbol: 'AAPL' }]
})
);
@@ -961,13 +974,17 @@ describe('PortfolioService', () => {
count: 2
});
- jest.spyOn(dataProviderService, 'getQuotes').mockResolvedValue({
- 'YAHOO-AAPL': {
- currency: 'USD',
- dataSource: DataSource.YAHOO,
- marketPrice: 200,
- marketState: 'open'
- }
+ jest.spyOn(currentRateService, 'getValues').mockResolvedValue({
+ dataProviderInfos: [],
+ errors: [],
+ values: [
+ {
+ dataSource: DataSource.YAHOO,
+ date: new Date(),
+ marketPrice: 200,
+ symbol: 'AAPL'
+ }
+ ]
});
jest
@@ -1018,7 +1035,11 @@ describe('PortfolioService', () => {
count: 2
});
- jest.spyOn(dataProviderService, 'getQuotes').mockResolvedValue({});
+ jest.spyOn(currentRateService, 'getValues').mockResolvedValue({
+ dataProviderInfos: [],
+ errors: [],
+ values: []
+ });
const summary = await getSummary({
balanceInBaseCurrency: 1000,
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index e5866ca3b2..21e615bc21 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -94,18 +94,21 @@ import {
import { Big } from 'big.js';
import {
differenceInDays,
+ endOfDay,
format,
isAfter,
isBefore,
isSameMonth,
isSameYear,
parseISO,
- set
+ set,
+ startOfDay
} from 'date-fns';
import { groupBy } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator';
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
+import { CurrentRateService } from './current-rate.service';
import { RulesService } from './rules.service';
const Fuse = require('fuse.js');
@@ -125,6 +128,7 @@ export class PortfolioService {
private readonly activitiesService: ActivitiesService,
private readonly benchmarkService: BenchmarkService,
private readonly calculatorFactory: PortfolioCalculatorFactory,
+ private readonly currentRateService: CurrentRateService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly i18nService: I18nService,
@@ -2539,11 +2543,15 @@ export class PortfolioService {
return !quantity.eq(0);
});
- const quotes =
+ const now = new Date();
+
+ // Get the market prices of today with the same fallback as the portfolio
+ // calculator
+ const { values } =
openHoldings.length > 0
- ? await this.dataProviderService.getQuotes({
+ ? await this.currentRateService.getValues({
subscriptionType,
- items: openHoldings.map(
+ dataGatheringItems: openHoldings.map(
({
latestActivity: {
assetProfile: { dataSource, symbol }
@@ -2551,16 +2559,24 @@ export class PortfolioService {
}) => {
return { dataSource, symbol };
}
- )
+ ),
+ dateQuery: {
+ gte: startOfDay(now),
+ lt: endOfDay(now)
+ }
})
- : {};
+ : { values: [] };
return getSum(
openHoldings.map(({ latestActivity, quantity }) => {
const { assetProfile, currency, unitPrice } = latestActivity;
- const marketPrice =
- quotes[getAssetProfileIdentifier(assetProfile)]?.marketPrice;
+ const marketPrice = values.find((value) => {
+ return (
+ getAssetProfileIdentifier(value) ===
+ getAssetProfileIdentifier(assetProfile)
+ );
+ })?.marketPrice;
if (!marketPrice) {
// Fall back to the unit price of the latest activity without a
diff --git a/apps/api/src/services/data-provider/manual/manual.service.spec.ts b/apps/api/src/services/data-provider/manual/manual.service.spec.ts
new file mode 100644
index 0000000000..2fc69b8b2d
--- /dev/null
+++ b/apps/api/src/services/data-provider/manual/manual.service.spec.ts
@@ -0,0 +1,96 @@
+import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
+import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
+import { EnhancedAssetProfile } from '@ghostfolio/common/interfaces';
+
+import { DataSource } from '@prisma/client';
+
+import { ManualService } from './manual.service';
+
+jest.mock('@ghostfolio/api/services/fetch/fetch.service', () => {
+ return {
+ FetchService: jest.fn()
+ };
+});
+
+describe('ManualService', () => {
+ let manualService: ManualService;
+ let prismaService: {
+ marketData: {
+ findFirst: jest.Mock;
+ };
+ };
+ let symbolProfileService: SymbolProfileService;
+
+ beforeEach(() => {
+ prismaService = {
+ marketData: {
+ findFirst: jest.fn()
+ }
+ };
+
+ symbolProfileService = new SymbolProfileService(null);
+
+ manualService = new ManualService(
+ null,
+ null,
+ prismaService as unknown as PrismaService,
+ symbolProfileService
+ );
+ });
+
+ describe('getQuotes', () => {
+ it('should use the latest market price of each symbol of the manual data source', async () => {
+ const latestMarketPrices: { [symbol: string]: number } = {
+ A: 13,
+ B: 22
+ };
+
+ prismaService.marketData.findFirst.mockImplementation(
+ ({ where: { symbol } }) => {
+ return Promise.resolve(
+ latestMarketPrices[symbol]
+ ? { symbol, marketPrice: latestMarketPrices[symbol] }
+ : null
+ );
+ }
+ );
+
+ jest.spyOn(symbolProfileService, 'getSymbolProfiles').mockResolvedValue(
+ ['A', 'B', 'C'].map((symbol) => {
+ return { symbol, currency: 'USD' };
+ }) as EnhancedAssetProfile[]
+ );
+
+ const quotes = await manualService.getQuotes({
+ symbols: ['A', 'B', 'C']
+ });
+
+ expect(prismaService.marketData.findFirst).toHaveBeenCalledWith({
+ orderBy: { date: 'desc' },
+ where: { dataSource: DataSource.MANUAL, symbol: 'A' }
+ });
+
+ expect(quotes).toEqual({
+ A: {
+ currency: 'USD',
+ dataSource: DataSource.MANUAL,
+ marketPrice: 13,
+ marketState: 'delayed'
+ },
+ B: {
+ currency: 'USD',
+ dataSource: DataSource.MANUAL,
+ marketPrice: 22,
+ marketState: 'delayed'
+ },
+ // Without market data
+ C: {
+ currency: 'USD',
+ dataSource: DataSource.MANUAL,
+ marketPrice: 0,
+ marketState: 'delayed'
+ }
+ });
+ });
+ });
+});
diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts
index a187335a0e..30168cdef8 100644
--- a/apps/api/src/services/data-provider/manual/manual.service.ts
+++ b/apps/api/src/services/data-provider/manual/manual.service.ts
@@ -150,18 +150,21 @@ export class ManualService implements DataProviderInterface {
})
);
- const marketData = await this.prismaService.marketData.findMany({
- distinct: ['symbol'],
- orderBy: {
- date: 'desc'
- },
- take: symbols.length,
- where: {
- symbol: {
- in: symbols
- }
- }
- });
+ // Query the latest market data per symbol because distinct loads all
+ // the market data of the symbols into memory
+ const marketData = await Promise.all(
+ symbols.map((symbol) => {
+ return this.prismaService.marketData.findFirst({
+ orderBy: {
+ date: 'desc'
+ },
+ where: {
+ symbol,
+ dataSource: this.getName()
+ }
+ });
+ })
+ );
const symbolProfilesToScrape = symbolProfiles.filter(
({ scraperConfiguration }) => {
@@ -202,7 +205,7 @@ export class ManualService implements DataProviderInterface {
marketPrice =
marketPrice ??
marketData.find((marketDataItem) => {
- return marketDataItem.symbol === symbol;
+ return marketDataItem?.symbol === symbol;
})?.marketPrice ??
0;
From 1a744292741fbba78f6bf6ff6b6c9219260fa179 Mon Sep 17 00:00:00 2001
From: David Requeno <108202767+DavidReque@users.noreply.github.com>
Date: Mon, 28 Sep 2026 08:03:18 -0600
Subject: [PATCH 6/7] Task/migrate ESLint configuration to flat config format
without FlatCompat (#7938)
* Migrate ESLint configuration to flat config format without FlatCompat
* Update changelog
---
CHANGELOG.md | 1 +
apps/client/eslint.config.cjs | 2 +-
eslint.config.cjs | 290 +++++++++++++++-------------------
libs/ui/eslint.config.cjs | 2 +-
package-lock.json | 26 ++-
package.json | 2 +-
6 files changed, 159 insertions(+), 164 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a03fdc09a3..e5036ab1ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the get quotes functionality of the _Manual_ service
+- Migrated the _ESLint_ configuration to the flat config format without `FlatCompat`
- Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2`
### Fixed
diff --git a/apps/client/eslint.config.cjs b/apps/client/eslint.config.cjs
index bcbe2c1c13..b2eb1117ce 100644
--- a/apps/client/eslint.config.cjs
+++ b/apps/client/eslint.config.cjs
@@ -1,6 +1,6 @@
const baseConfig = require('../../eslint.config.cjs');
const angularEslintPlugin = require('@angular-eslint/eslint-plugin');
-const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
+const typescriptEslintPlugin = require('typescript-eslint').plugin;
module.exports = [
{
diff --git a/eslint.config.cjs b/eslint.config.cjs
index 76d627d182..9c1b2024b6 100644
--- a/eslint.config.cjs
+++ b/eslint.config.cjs
@@ -1,12 +1,7 @@
-const { FlatCompat } = require('@eslint/eslintrc');
-const js = require('@eslint/js');
const nxEslintPlugin = require('@nx/eslint-plugin');
+const importPlugin = require('eslint-plugin-import');
const storybook = require('eslint-plugin-storybook');
-
-const compat = new FlatCompat({
- baseDirectory: __dirname,
- recommendedConfig: js.configs.recommended
-});
+const typescriptEslint = require('typescript-eslint');
module.exports = [
{
@@ -35,160 +30,135 @@ module.exports = [
'no-extra-semi': 'off'
}
},
- ...compat
- .config({
- extends: ['plugin:@nx/typescript']
- })
- .map((config) => ({
- ...config,
- files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'],
- rules: {
- ...config.rules
- }
- })),
- ...compat
- .config({
- extends: ['plugin:@nx/javascript']
- })
- .map((config) => ({
- ...config,
- files: ['**/*.js', '**/*.jsx', '**/*.cjs', '**/*.mjs'],
- rules: {
- ...config.rules
- }
- })),
- ...compat
- .config({
- plugins: ['eslint-plugin-import', '@typescript-eslint'],
- extends: [
- 'plugin:@typescript-eslint/recommended-type-checked',
- 'plugin:@typescript-eslint/stylistic-type-checked'
- ]
- })
- .map((config) => ({
- ...config,
- files: ['**/*.ts'],
- rules: {
- ...config.rules,
- '@typescript-eslint/consistent-indexed-object-style': 'off',
- '@typescript-eslint/dot-notation': 'off',
- '@typescript-eslint/explicit-member-accessibility': [
- 'off',
- {
- accessibility: 'explicit'
- }
- ],
- '@typescript-eslint/member-ordering': 'warn',
- '@typescript-eslint/naming-convention': [
- 'off',
- {
- selector: 'default',
- format: ['camelCase'],
- leadingUnderscore: 'allow',
- trailingUnderscore: 'allow'
- },
- {
- selector: ['variable', 'classProperty', 'typeProperty'],
- format: ['camelCase', 'UPPER_CASE'],
- leadingUnderscore: 'allow',
- trailingUnderscore: 'allow'
- },
- {
- selector: 'objectLiteralProperty',
- format: null
- },
- {
- selector: 'enumMember',
- format: ['camelCase', 'UPPER_CASE', 'PascalCase']
- },
- {
- selector: 'typeLike',
- format: ['PascalCase']
- }
- ],
- '@typescript-eslint/no-empty-interface': 'warn',
- '@typescript-eslint/no-inferrable-types': [
- 'warn',
- {
- ignoreParameters: true
- }
- ],
- '@typescript-eslint/no-non-null-assertion': 'warn',
- '@typescript-eslint/no-shadow': [
- 'warn',
- {
- hoist: 'all'
- }
- ],
- '@typescript-eslint/unified-signatures': 'error',
- '@typescript-eslint/no-loss-of-precision': 'warn',
- '@typescript-eslint/no-var-requires': 'warn',
- 'arrow-body-style': 'off',
- 'constructor-super': 'error',
- eqeqeq: ['error', 'smart'],
- 'guard-for-in': 'warn',
- 'id-blacklist': 'off',
- 'id-match': 'off',
- 'import/no-deprecated': 'warn',
- 'no-bitwise': 'error',
- 'no-caller': 'error',
- 'no-debugger': 'error',
- 'no-empty': 'off',
- 'no-eval': 'error',
- 'no-fallthrough': 'error',
- 'no-new-wrappers': 'error',
- 'no-restricted-imports': ['error', 'rxjs/Rx'],
- 'no-undef-init': 'error',
- 'no-underscore-dangle': 'off',
- 'no-var': 'error',
- radix: 'error',
- 'no-unsafe-optional-chaining': 'warn',
- 'no-extra-boolean-cast': 'warn',
- 'no-empty-pattern': 'warn',
- 'no-useless-catch': 'warn',
- 'no-unsafe-finally': 'warn',
- 'no-prototype-builtins': 'warn',
- 'no-async-promise-executor': 'warn',
- 'no-constant-condition': 'warn',
+ ...nxEslintPlugin.configs['flat/typescript'],
+ ...nxEslintPlugin.configs['flat/javascript'],
+ ...typescriptEslint.config({
+ files: ['**/*.ts'],
+ plugins: { import: importPlugin },
+ extends: [
+ typescriptEslint.configs.recommendedTypeChecked,
+ typescriptEslint.configs.stylisticTypeChecked
+ ],
+ rules: {
+ '@typescript-eslint/consistent-indexed-object-style': 'off',
+ '@typescript-eslint/dot-notation': 'off',
+ '@typescript-eslint/explicit-member-accessibility': [
+ 'off',
+ {
+ accessibility: 'explicit'
+ }
+ ],
+ '@typescript-eslint/member-ordering': 'warn',
+ '@typescript-eslint/naming-convention': [
+ 'off',
+ {
+ selector: 'default',
+ format: ['camelCase'],
+ leadingUnderscore: 'allow',
+ trailingUnderscore: 'allow'
+ },
+ {
+ selector: ['variable', 'classProperty', 'typeProperty'],
+ format: ['camelCase', 'UPPER_CASE'],
+ leadingUnderscore: 'allow',
+ trailingUnderscore: 'allow'
+ },
+ {
+ selector: 'objectLiteralProperty',
+ format: null
+ },
+ {
+ selector: 'enumMember',
+ format: ['camelCase', 'UPPER_CASE', 'PascalCase']
+ },
+ {
+ selector: 'typeLike',
+ format: ['PascalCase']
+ }
+ ],
+ '@typescript-eslint/no-empty-interface': 'warn',
+ '@typescript-eslint/no-inferrable-types': [
+ 'warn',
+ {
+ ignoreParameters: true
+ }
+ ],
+ '@typescript-eslint/no-non-null-assertion': 'warn',
+ '@typescript-eslint/no-shadow': [
+ 'warn',
+ {
+ hoist: 'all'
+ }
+ ],
+ '@typescript-eslint/unified-signatures': 'error',
+ '@typescript-eslint/no-loss-of-precision': 'warn',
+ '@typescript-eslint/no-var-requires': 'warn',
+ 'arrow-body-style': 'off',
+ 'constructor-super': 'error',
+ eqeqeq: ['error', 'smart'],
+ 'guard-for-in': 'warn',
+ 'id-blacklist': 'off',
+ 'id-match': 'off',
+ 'import/no-deprecated': 'warn',
+ 'no-bitwise': 'error',
+ 'no-caller': 'error',
+ 'no-debugger': 'error',
+ 'no-empty': 'off',
+ 'no-eval': 'error',
+ 'no-fallthrough': 'error',
+ 'no-new-wrappers': 'error',
+ 'no-restricted-imports': ['error', 'rxjs/Rx'],
+ 'no-undef-init': 'error',
+ 'no-underscore-dangle': 'off',
+ 'no-var': 'error',
+ radix: 'error',
+ 'no-unsafe-optional-chaining': 'warn',
+ 'no-extra-boolean-cast': 'warn',
+ 'no-empty-pattern': 'warn',
+ 'no-useless-catch': 'warn',
+ 'no-unsafe-finally': 'warn',
+ 'no-prototype-builtins': 'warn',
+ 'no-async-promise-executor': 'warn',
+ 'no-constant-condition': 'warn',
- // The following rules are part of eslint:recommended
- // and can be remove once solved
- 'no-loss-of-precision': 'warn',
+ // The following rules are part of eslint:recommended
+ // and can be remove once solved
+ 'no-loss-of-precision': 'warn',
- // The following rules are part of @typescript-eslint/recommended-type-checked
- // and can be remove once solved
- '@typescript-eslint/await-thenable': 'warn',
- '@typescript-eslint/ban-ts-comment': 'warn',
- '@typescript-eslint/no-base-to-string': 'warn',
- '@typescript-eslint/no-empty-object-type': 'warn',
- '@typescript-eslint/no-explicit-any': 'warn',
- '@typescript-eslint/no-floating-promises': 'warn',
- '@typescript-eslint/no-misused-promises': 'warn',
- '@typescript-eslint/no-redundant-type-constituents': 'warn',
- '@typescript-eslint/no-require-imports': 'warn',
- '@typescript-eslint/no-unnecessary-type-assertion': 'warn',
- '@typescript-eslint/no-unsafe-argument': 'warn',
- '@typescript-eslint/no-unsafe-assignment': 'warn',
- '@typescript-eslint/no-unsafe-enum-comparison': 'warn',
- '@typescript-eslint/no-unsafe-member-access': 'warn',
- '@typescript-eslint/no-unsafe-return': 'warn',
- '@typescript-eslint/no-unsafe-call': 'warn',
- '@typescript-eslint/no-unused-vars': [
- 'error',
- {
- caughtErrors: 'none'
- }
- ],
- '@typescript-eslint/no-wrapper-object-types': 'warn',
- '@typescript-eslint/only-throw-error': 'warn',
- '@typescript-eslint/prefer-promise-reject-errors': 'warn',
- '@typescript-eslint/require-await': 'warn',
- '@typescript-eslint/restrict-template-expressions': 'warn',
- '@typescript-eslint/unbound-method': 'warn',
+ // The following rules are part of @typescript-eslint/recommended-type-checked
+ // and can be remove once solved
+ '@typescript-eslint/await-thenable': 'warn',
+ '@typescript-eslint/ban-ts-comment': 'warn',
+ '@typescript-eslint/no-base-to-string': 'warn',
+ '@typescript-eslint/no-empty-object-type': 'warn',
+ '@typescript-eslint/no-explicit-any': 'warn',
+ '@typescript-eslint/no-floating-promises': 'warn',
+ '@typescript-eslint/no-misused-promises': 'warn',
+ '@typescript-eslint/no-redundant-type-constituents': 'warn',
+ '@typescript-eslint/no-require-imports': 'warn',
+ '@typescript-eslint/no-unnecessary-type-assertion': 'warn',
+ '@typescript-eslint/no-unsafe-argument': 'warn',
+ '@typescript-eslint/no-unsafe-assignment': 'warn',
+ '@typescript-eslint/no-unsafe-enum-comparison': 'warn',
+ '@typescript-eslint/no-unsafe-member-access': 'warn',
+ '@typescript-eslint/no-unsafe-return': 'warn',
+ '@typescript-eslint/no-unsafe-call': 'warn',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ caughtErrors: 'none'
+ }
+ ],
+ '@typescript-eslint/no-wrapper-object-types': 'warn',
+ '@typescript-eslint/only-throw-error': 'warn',
+ '@typescript-eslint/prefer-promise-reject-errors': 'warn',
+ '@typescript-eslint/require-await': 'warn',
+ '@typescript-eslint/restrict-template-expressions': 'warn',
+ '@typescript-eslint/unbound-method': 'warn',
- // The following rules are part of @typescript-eslint/stylistic-type-checked
- // and can be remove once solved
- '@typescript-eslint/prefer-nullish-coalescing': 'warn' // TODO: Requires strictNullChecks: true
- }
- }))
+ // The following rules are part of @typescript-eslint/stylistic-type-checked
+ // and can be remove once solved
+ '@typescript-eslint/prefer-nullish-coalescing': 'warn' // TODO: Requires strictNullChecks: true
+ }
+ })
];
diff --git a/libs/ui/eslint.config.cjs b/libs/ui/eslint.config.cjs
index b7aeb37e03..70a725c528 100644
--- a/libs/ui/eslint.config.cjs
+++ b/libs/ui/eslint.config.cjs
@@ -2,7 +2,7 @@ const baseConfig = require('../../eslint.config.cjs');
const angularEslintPlugin = require('@angular-eslint/eslint-plugin');
const angularTemplateEslintPlugin = require('@angular-eslint/eslint-plugin-template');
const angularTemplateParser = require('@angular-eslint/template-parser');
-const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
+const typescriptEslintPlugin = require('typescript-eslint').plugin;
module.exports = [
{
diff --git a/package-lock.json b/package-lock.json
index 0bccf99a2b..c4896f0436 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -122,7 +122,6 @@
"@angular/language-service": "22.1.2",
"@angular/localize": "22.1.2",
"@angular/pwa": "22.1.4",
- "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.2.3",
@@ -177,6 +176,7 @@
"ts-node": "10.9.2",
"tslib": "2.8.1",
"typescript": "6.0.3",
+ "typescript-eslint": "8.67.0",
"webpack-cli": "7.1.0"
},
"engines": {
@@ -34933,6 +34933,30 @@
"node": ">=14.17"
}
},
+ "node_modules/typescript-eslint": {
+ "version": "8.67.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz",
+ "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.67.0",
+ "@typescript-eslint/parser": "8.67.0",
+ "@typescript-eslint/typescript-estree": "8.67.0",
+ "@typescript-eslint/utils": "8.67.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
diff --git a/package.json b/package.json
index bb1c66a955..3361c354ba 100644
--- a/package.json
+++ b/package.json
@@ -166,7 +166,6 @@
"@angular/language-service": "22.1.2",
"@angular/localize": "22.1.2",
"@angular/pwa": "22.1.4",
- "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.2.3",
@@ -221,6 +220,7 @@
"ts-node": "10.9.2",
"tslib": "2.8.1",
"typescript": "6.0.3",
+ "typescript-eslint": "8.67.0",
"webpack-cli": "7.1.0"
},
"engines": {
From 26cce42375993c3a8d1ce35d4ef87d36c3bdb605 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Mon, 28 Sep 2026 17:54:38 +0200
Subject: [PATCH 7/7] Feature/include dividends in portfolio performance
(#7961)
* Include dividends in portfolio performance
* Update changelog
---
CHANGELOG.md | 2 +
.../src/app/portfolio/portfolio.service.ts | 2 +
.../portfolio-summary.component.html | 58 ++++++++-----
.../portfolio-summary.component.ts | 31 +++++++
.../analysis/analysis-page.component.ts | 30 +++++++
.../portfolio/analysis/analysis-page.html | 85 +++++++++----------
6 files changed, 144 insertions(+), 64 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5036ab1ab..46ffe1b2ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Extended the net performance on the analysis page to include the dividends (experimental)
+- Extended the net performance in the portfolio summary to include the dividends (experimental)
- Improved the get quotes functionality of the _Manual_ service
- Migrated the _ESLint_ configuration to the flat config format without `FlatCompat`
- Upgraded `chartjs-chart-treemap` from version `4.2.0` to `4.2.2`
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 21e615bc21..7350f5fc01 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -2140,6 +2140,7 @@ export class PortfolioService {
const {
currentValueInBaseCurrency,
+ dividendPercentageWithCurrencyEffect,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect,
@@ -2236,6 +2237,7 @@ export class PortfolioService {
cash,
currentValueInBaseCurrency,
dateOfFirstActivity,
+ dividendPercentageWithCurrencyEffect,
excludedAccountsAndActivities,
netPerformance,
netPerformancePercentage,
diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
index 8846c14a7c..2130493f03 100644
--- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
+++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
@@ -98,10 +98,29 @@
/>
+ @if (user?.settings?.isExperimentalFeatures) {
+
+
Dividend
+
+ @if (isNumber(summary?.dividendInBaseCurrency)) {
+ +
+ }
+
+
+
+ }
Fees
- @if (summary?.fees || summary?.fees === 0) {
+ @if (isNumber(summary?.fees)) {
-
}
@@ -154,7 +173,7 @@
[isLoading]="isLoading"
[isPercent]="true"
[locale]="locale"
- [value]="summary?.netPerformancePercentageWithCurrencyEffect"
+ [value]="netPerformancePercentageWithCurrencyEffect"
/>
@@ -367,10 +386,7 @@
Liabilities
- @if (
- summary?.liabilitiesInBaseCurrency ||
- summary?.liabilitiesInBaseCurrency === 0
- ) {
+ @if (isNumber(summary?.liabilitiesInBaseCurrency)) {
-
}
-
-
Dividend
-
-
+ @if (!user?.settings?.isExperimentalFeatures) {
+
-
+ }
diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
index dec65ddad9..23e32a0c9c 100644
--- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
+++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
@@ -55,6 +55,7 @@ export class GfPortfolioSummaryComponent implements OnChanges {
'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
);
+ protected readonly isNumber = isNumber;
protected readonly PerformanceCalculationType = PerformanceCalculationType;
protected isCashExpanded = false;
@@ -136,6 +137,36 @@ export class GfPortfolioSummaryComponent implements OnChanges {
);
}
+ protected get netPerformancePercentageWithCurrencyEffect() {
+ if (
+ !this.user?.settings?.isExperimentalFeatures ||
+ !isNumber(this.summary?.netPerformancePercentageWithCurrencyEffect)
+ ) {
+ return this.summary?.netPerformancePercentageWithCurrencyEffect;
+ }
+
+ // Include the dividends (total return)
+ return (
+ this.summary.netPerformancePercentageWithCurrencyEffect +
+ (this.summary.dividendPercentageWithCurrencyEffect ?? 0)
+ );
+ }
+
+ protected get netPerformanceWithCurrencyEffect() {
+ if (
+ !this.user?.settings?.isExperimentalFeatures ||
+ !isNumber(this.summary?.netPerformanceWithCurrencyEffect)
+ ) {
+ return this.summary?.netPerformanceWithCurrencyEffect;
+ }
+
+ // Include the dividends (total return)
+ return (
+ this.summary.netPerformanceWithCurrencyEffect +
+ this.summary.dividendInBaseCurrency
+ );
+ }
+
public ngOnChanges() {
if (this.summary) {
if (
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
index aa57e3d9c8..450749925e 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
@@ -133,6 +133,36 @@ export class GfAnalysisPageComponent implements OnInit {
addIcons({ copyOutline, ellipsisVertical });
}
+ protected get netPerformancePercentageWithCurrencyEffect() {
+ if (
+ !this.user?.settings?.isExperimentalFeatures ||
+ !isNumber(this.performance?.netPerformancePercentageWithCurrencyEffect)
+ ) {
+ return this.performance?.netPerformancePercentageWithCurrencyEffect;
+ }
+
+ // Include the dividends (total return)
+ return (
+ this.performance.netPerformancePercentageWithCurrencyEffect +
+ (this.performance.dividendPercentageWithCurrencyEffect ?? 0)
+ );
+ }
+
+ protected get netPerformanceWithCurrencyEffect() {
+ if (
+ !this.user?.settings?.isExperimentalFeatures ||
+ !isNumber(this.performance?.netPerformanceWithCurrencyEffect)
+ ) {
+ return this.performance?.netPerformanceWithCurrencyEffect;
+ }
+
+ // Include the dividends (total return)
+ return (
+ this.performance.netPerformanceWithCurrencyEffect +
+ this.performance.dividendInBaseCurrency
+ );
+ }
+
get savingsRate() {
if (!this.savingsRatePerMonth) {
return undefined;
diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
index 94a0328755..47cee7af6e 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
@@ -187,6 +187,45 @@
/>
+ @if (user?.settings?.isExperimentalFeatures) {
+
+
+ Absolute Dividend Performance
+ @if (user?.subscription?.type === 'Basic') {
+
+ }
+
+
+
+
+
+
+
+ Dividend Performance
+
+
+
+
+
+ }
@@ -273,52 +312,10 @@
[isLoading]="isLoadingInvestmentChart"
[isPercent]="true"
[locale]="user?.settings?.locale"
- [value]="
- performance?.netPerformancePercentageWithCurrencyEffect
- "
+ [value]="netPerformancePercentageWithCurrencyEffect"
/>
- @if (user?.settings?.isExperimentalFeatures) {
-
-
-
- Absolute Dividend Performance
- @if (user?.subscription?.type === 'Basic') {
-
- }
-
-
-
-
-
-
-
- Dividend Performance
-
-
-
-
-
- }