From 430f2fe06ff5fb3f89693643a890d42d41314727 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 1 Oct 2026 11:06:59 +0200 Subject: [PATCH 01/35] Task/load activities of portfolio calculator tests from export files (#7992) Load activities from export files --- .../portfolio-calculator-test-utils.ts | 40 ++++++++ ...tfolio-calculator-baln-buy-and-buy.spec.ts | 43 +-------- ...folio-calculator-baln-buy-and-sell.spec.ts | 43 +-------- .../portfolio-calculator-baln-buy.spec.ts | 94 +++---------------- ...ator-btcusd-buy-and-sell-partially.spec.ts | 43 +-------- .../portfolio-calculator-btcusd-short.spec.ts | 35 +------ .../roai/portfolio-calculator-btcusd.spec.ts | 35 +------ ...jnug-buy-and-sell-and-buy-and-sell.spec.ts | 37 +------- ...-calculator-msft-buy-with-dividend.spec.ts | 43 +-------- ...ulator-novn-buy-and-sell-partially.spec.ts | 37 +------- ...folio-calculator-novn-buy-and-sell.spec.ts | 37 +------- test/import/ok/baln-buy-and-buy.json | 36 +++++++ test/import/ok/baln-buy-and-sell.json | 36 +++++++ ...h-unit-price-lower-than-closing-price.json | 25 +++++ test/import/ok/baln-buy.json | 25 +++++ .../ok/btcusd-buy-and-sell-partially.json | 36 +++++++ test/import/ok/msft-buy-with-dividend.json | 36 +++++++ 17 files changed, 289 insertions(+), 392 deletions(-) create mode 100644 test/import/ok/baln-buy-and-buy.json create mode 100644 test/import/ok/baln-buy-and-sell.json create mode 100644 test/import/ok/baln-buy-with-unit-price-lower-than-closing-price.json create mode 100644 test/import/ok/baln-buy.json create mode 100644 test/import/ok/btcusd-buy-and-sell-partially.json create mode 100644 test/import/ok/msft-buy-with-dividend.json diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts index 87474e806c..fb88c8a7f1 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -1,12 +1,15 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; +import { parseDate } from '@ghostfolio/common/helper'; import { + Activity, ExportResponse, HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { DateRange } from '@ghostfolio/common/types'; import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; export const activityDummyData = { accountId: undefined, @@ -71,6 +74,43 @@ export async function getPerformanceByDateRange({ return performanceByDateRange; } +/** + * Loads the activities of an export file in test/import/ok for the portfolio + * calculator. The user currency of the file is the base currency of the test. + * + * The activity currency must be the asset profile currency. The helper does + * not convert the fee to the base currency and does not load the tags. + */ +export function loadActivitiesFromExportFile(fileName: string): { + activities: Activity[]; + userCurrency: string; +} { + const exportResponse = loadExportFile( + join(__dirname, '../../../../../../test/import/ok', fileName) + ); + + const activities: Activity[] = exportResponse.activities.map((activity) => { + return { + ...activityDummyData, + ...activity, + assetProfile: { + ...assetProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: activity.symbol, + symbol: activity.symbol + }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, + tags: [], + unitPriceInAssetProfileCurrency: activity.unitPrice + }; + }); + + return { activities, userCurrency: exportResponse.user.settings.currency }; +} + export function loadExportFile(filePath: string): ExportResponse { return JSON.parse(readFileSync(filePath, 'utf8')); } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index 1b2fee98d9..5d3cbcf320 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -1,6 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -13,7 +12,6 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; @@ -85,45 +83,14 @@ describe('PortfolioCalculator', () => { it.only('with BALN.SW buy and buy', async () => { jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 142.9 - }, - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 136.6 - } - ]; + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'baln-buy-and-buy.json' + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index 408076717f..9ee4877180 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -1,7 +1,6 @@ import { - activityDummyData, - assetProfileDummyData, getPerformanceByDateRange, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,7 +13,6 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; @@ -86,45 +84,14 @@ describe('PortfolioCalculator', () => { it.only('with BALN.SW buy and sell', async () => { jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 142.9 - }, - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 2, - type: 'SELL', - unitPriceInAssetProfileCurrency: 136.6 - } - ]; + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'baln-buy-and-sell.json' + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, usePortfolioSnapshotCache: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index 19f3c64369..afae22c1c6 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -1,6 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -13,7 +12,6 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; @@ -85,29 +83,13 @@ describe('PortfolioCalculator', () => { it.only('with BALN.SW buy', async () => { jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 136.6 - } - ]; + const { activities, userCurrency } = + loadActivitiesFromExportFile('baln-buy.json'); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, userId: userDummyData.id }); @@ -217,29 +199,14 @@ describe('PortfolioCalculator', () => { it.only('with BALN.SW buy (with unit price lower than closing price)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 135.0 - } - ]; + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'baln-buy-with-unit-price-lower-than-closing-price.json' + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, userId: userDummyData.id }); @@ -257,29 +224,14 @@ describe('PortfolioCalculator', () => { it.only('with BALN.SW buy (with unit price lower than closing price), calculated on buy date', async () => { jest.useFakeTimers().setSystemTime(parseDate('2021-11-30').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 135.0 - } - ]; + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'baln-buy-with-unit-price-lower-than-closing-price.json' + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, userId: userDummyData.id }); @@ -297,29 +249,13 @@ describe('PortfolioCalculator', () => { it.only('with BALN.SW buy (on a date without historical market data)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 136.6 - } - ]; + const { activities, userCurrency } = + loadActivitiesFromExportFile('baln-buy.json'); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, userId: userDummyData.id }); 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 6a9ed1f71c..15474a0236 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 @@ -1,7 +1,6 @@ import { - activityDummyData, - assetProfileDummyData, getPerformanceByDateRange, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -15,7 +14,6 @@ import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-r 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; @@ -98,45 +96,14 @@ describe('PortfolioCalculator', () => { it.only('with BTCUSD buy and sell partially', async () => { jest.useFakeTimers().setSystemTime(parseDate('2018-01-01').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Bitcoin USD', - symbol: 'BTCUSD' - }, - date: new Date('2015-01-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 2, - type: 'BUY', - unitPriceInAssetProfileCurrency: 320.43 - }, - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Bitcoin USD', - symbol: 'BTCUSD' - }, - date: new Date('2017-12-31'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - type: 'SELL', - unitPriceInAssetProfileCurrency: 14156.4 - } - ]; + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'btcusd-buy-and-sell-partially.json' + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: userCurrency, usePortfolioSnapshotCache: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts index 58dc545e49..356fdebd03 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts @@ -1,7 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, - loadExportFile, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,11 +12,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; -import { join } from 'node:path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -48,8 +44,6 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -57,12 +51,6 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; - beforeAll(() => { - exportResponse = loadExportFile( - join(__dirname, '../../../../../../../test/import/ok/btcusd-short.json') - ); - }); - beforeEach(() => { PortfolioSnapshotServiceMock.reset(); RedisCacheServiceMock.reset(); @@ -95,28 +83,13 @@ describe('PortfolioCalculator', () => { it.only('with BTCUSD short sell (in USD)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - assetProfile: { - ...assetProfileDummyData, - currency: 'USD', - dataSource: activity.dataSource, - name: 'Bitcoin', - symbol: activity.symbol - }, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) - ); + const { activities, userCurrency } = + loadActivitiesFromExportFile('btcusd-short.json'); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, + currency: userCurrency, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index e0cfeb8227..0d0f6cfc3b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -1,7 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, - loadExportFile, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,11 +12,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; -import { join } from 'node:path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -48,8 +44,6 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -57,12 +51,6 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; - beforeAll(() => { - exportResponse = loadExportFile( - join(__dirname, '../../../../../../../test/import/ok/btcusd.json') - ); - }); - beforeEach(() => { PortfolioSnapshotServiceMock.reset(); RedisCacheServiceMock.reset(); @@ -95,28 +83,13 @@ describe('PortfolioCalculator', () => { it.only('with BTCUSD buy (in USD)', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - assetProfile: { - ...assetProfileDummyData, - currency: 'USD', - dataSource: activity.dataSource, - name: 'Bitcoin', - symbol: activity.symbol - }, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 4.46, - unitPriceInAssetProfileCurrency: 44558.42 - }) - ); + const { activities, userCurrency } = + loadActivitiesFromExportFile('btcusd.json'); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, + currency: userCurrency, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts index ba2b7d05c6..b2e0ffab6b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts @@ -1,7 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, - loadExportFile, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,11 +12,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; -import { join } from 'node:path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -48,8 +44,6 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -57,15 +51,6 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; - beforeAll(() => { - exportResponse = loadExportFile( - join( - __dirname, - '../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json' - ) - ); - }); - beforeEach(() => { PortfolioSnapshotServiceMock.reset(); RedisCacheServiceMock.reset(); @@ -98,28 +83,14 @@ describe('PortfolioCalculator', () => { it.only('with JNUG buy and sell', async () => { jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - assetProfile: { - ...assetProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', - symbol: activity.symbol - }, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'jnug-buy-and-sell-and-buy-and-sell.json' ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, + currency: userCurrency, userId: userDummyData.id }); 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 2c6ff79b6d..649542e325 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 @@ -1,7 +1,6 @@ import { - activityDummyData, - assetProfileDummyData, getPerformanceByDateRange, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,7 +13,6 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; @@ -86,45 +84,14 @@ describe('PortfolioCalculator', () => { it.only('with MSFT buy', async () => { jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - date: new Date('2021-09-16'), - feeInAssetProfileCurrency: 19, - feeInBaseCurrency: 19, - quantity: 1, - type: 'BUY', - unitPriceInAssetProfileCurrency: 298.58 - }, - { - ...activityDummyData, - assetProfile: { - ...assetProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - date: new Date('2021-11-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.62 - } - ]; + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'msft-buy-with-dividend.json' + ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'USD', + currency: userCurrency, usePortfolioSnapshotCache: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 92d078d811..e85a053e61 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -1,7 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, - loadExportFile, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,11 +12,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; -import { join } from 'node:path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -48,8 +44,6 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -57,15 +51,6 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; - beforeAll(() => { - exportResponse = loadExportFile( - join( - __dirname, - '../../../../../../../test/import/ok/novn-buy-and-sell-partially.json' - ) - ); - }); - beforeEach(() => { PortfolioSnapshotServiceMock.reset(); RedisCacheServiceMock.reset(); @@ -98,28 +83,14 @@ describe('PortfolioCalculator', () => { it.only('with NOVN.SW buy and sell partially', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - assetProfile: { - ...assetProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Novartis AG', - symbol: activity.symbol - }, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'novn-buy-and-sell-partially.json' ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, + currency: userCurrency, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index 46e1b14aa4..a0174b048e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -1,7 +1,5 @@ import { - activityDummyData, - assetProfileDummyData, - loadExportFile, + loadActivitiesFromExportFile, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -14,11 +12,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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 { parseDate } from '@ghostfolio/common/helper'; -import { Activity, ExportResponse } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; -import { join } from 'node:path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -48,8 +44,6 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -57,15 +51,6 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; - beforeAll(() => { - exportResponse = loadExportFile( - join( - __dirname, - '../../../../../../../test/import/ok/novn-buy-and-sell.json' - ) - ); - }); - beforeEach(() => { PortfolioSnapshotServiceMock.reset(); RedisCacheServiceMock.reset(); @@ -98,28 +83,14 @@ describe('PortfolioCalculator', () => { it.only('with NOVN.SW buy and sell', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - assetProfile: { - ...assetProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Novartis AG', - symbol: activity.symbol - }, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) + const { activities, userCurrency } = loadActivitiesFromExportFile( + 'novn-buy-and-sell.json' ); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, + currency: userCurrency, userId: userDummyData.id }); diff --git a/test/import/ok/baln-buy-and-buy.json b/test/import/ok/baln-buy-and-buy.json new file mode 100644 index 0000000000..c5c33764b1 --- /dev/null +++ b/test/import/ok/baln-buy-and-buy.json @@ -0,0 +1,36 @@ +{ + "meta": { + "date": "2026-10-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2021-11-22T00:00:00.000Z", + "fee": 1.55, + "id": "fe7d42ee-e607-58b7-a183-9f2f894fe204", + "quantity": 2, + "symbol": "BALN.SW", + "type": "BUY", + "unitPrice": 142.9 + }, + { + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2021-11-30T00:00:00.000Z", + "fee": 1.65, + "id": "716b3742-6326-5fb1-a420-5f21b97316ea", + "quantity": 2, + "symbol": "BALN.SW", + "type": "BUY", + "unitPrice": 136.6 + } + ], + "user": { + "settings": { + "currency": "CHF", + "performanceCalculationType": "ROAI" + } + } +} diff --git a/test/import/ok/baln-buy-and-sell.json b/test/import/ok/baln-buy-and-sell.json new file mode 100644 index 0000000000..7bb096e51c --- /dev/null +++ b/test/import/ok/baln-buy-and-sell.json @@ -0,0 +1,36 @@ +{ + "meta": { + "date": "2026-10-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2021-11-22T00:00:00.000Z", + "fee": 1.55, + "id": "9269a332-dec6-51b5-9c9d-60dec1cf3d25", + "quantity": 2, + "symbol": "BALN.SW", + "type": "BUY", + "unitPrice": 142.9 + }, + { + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2021-11-30T00:00:00.000Z", + "fee": 1.65, + "id": "aafb6d62-3486-5106-bff5-9e06b91b8d89", + "quantity": 2, + "symbol": "BALN.SW", + "type": "SELL", + "unitPrice": 136.6 + } + ], + "user": { + "settings": { + "currency": "CHF", + "performanceCalculationType": "ROAI" + } + } +} diff --git a/test/import/ok/baln-buy-with-unit-price-lower-than-closing-price.json b/test/import/ok/baln-buy-with-unit-price-lower-than-closing-price.json new file mode 100644 index 0000000000..bef2f1eaa8 --- /dev/null +++ b/test/import/ok/baln-buy-with-unit-price-lower-than-closing-price.json @@ -0,0 +1,25 @@ +{ + "meta": { + "date": "2026-10-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2021-11-30T00:00:00.000Z", + "fee": 1.55, + "id": "99a0844c-53ff-58b9-b120-e9339ea3ba22", + "quantity": 2, + "symbol": "BALN.SW", + "type": "BUY", + "unitPrice": 135.0 + } + ], + "user": { + "settings": { + "currency": "CHF", + "performanceCalculationType": "ROAI" + } + } +} diff --git a/test/import/ok/baln-buy.json b/test/import/ok/baln-buy.json new file mode 100644 index 0000000000..8435b79137 --- /dev/null +++ b/test/import/ok/baln-buy.json @@ -0,0 +1,25 @@ +{ + "meta": { + "date": "2026-10-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2021-11-30T00:00:00.000Z", + "fee": 1.55, + "id": "d82cae72-04b1-5beb-9d84-8e7740cd1b44", + "quantity": 2, + "symbol": "BALN.SW", + "type": "BUY", + "unitPrice": 136.6 + } + ], + "user": { + "settings": { + "currency": "CHF", + "performanceCalculationType": "ROAI" + } + } +} diff --git a/test/import/ok/btcusd-buy-and-sell-partially.json b/test/import/ok/btcusd-buy-and-sell-partially.json new file mode 100644 index 0000000000..b91741245d --- /dev/null +++ b/test/import/ok/btcusd-buy-and-sell-partially.json @@ -0,0 +1,36 @@ +{ + "meta": { + "date": "2026-10-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "currency": "USD", + "dataSource": "YAHOO", + "date": "2015-01-01T00:00:00.000Z", + "fee": 0, + "id": "0d9ac134-2329-5234-8b59-8aaa6424e79d", + "quantity": 2, + "symbol": "BTCUSD", + "type": "BUY", + "unitPrice": 320.43 + }, + { + "currency": "USD", + "dataSource": "YAHOO", + "date": "2017-12-31T00:00:00.000Z", + "fee": 0, + "id": "f7a55970-57b0-52b3-9846-c7c306645050", + "quantity": 1, + "symbol": "BTCUSD", + "type": "SELL", + "unitPrice": 14156.4 + } + ], + "user": { + "settings": { + "currency": "CHF", + "performanceCalculationType": "ROAI" + } + } +} diff --git a/test/import/ok/msft-buy-with-dividend.json b/test/import/ok/msft-buy-with-dividend.json new file mode 100644 index 0000000000..c4f20e2898 --- /dev/null +++ b/test/import/ok/msft-buy-with-dividend.json @@ -0,0 +1,36 @@ +{ + "meta": { + "date": "2026-10-01T00:00:00.000Z", + "version": "dev" + }, + "activities": [ + { + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-09-16T00:00:00.000Z", + "fee": 19, + "id": "dcec53cf-61d7-522b-985f-fd94b1f074a0", + "quantity": 1, + "symbol": "MSFT", + "type": "BUY", + "unitPrice": 298.58 + }, + { + "currency": "USD", + "dataSource": "YAHOO", + "date": "2021-11-16T00:00:00.000Z", + "fee": 0, + "id": "ac5970cf-4bb9-5385-8faf-9dd1efaca883", + "quantity": 1, + "symbol": "MSFT", + "type": "DIVIDEND", + "unitPrice": 0.62 + } + ], + "user": { + "settings": { + "currency": "USD", + "performanceCalculationType": "ROAI" + } + } +} From a168e77eb64149ac79bd6763ca866c5eb730af15 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 1 Oct 2026 19:42:38 +0200 Subject: [PATCH 02/35] Task/upgrade prettier to version 3.9.9 (#7988) * Update prettier to version 3.9.9 * 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 db21112dce..8e4b4ca6df 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 `prettier` from version `3.9.6` to `3.9.9` + ## 3.76.0 - 2026-09-30 ### Added diff --git a/package-lock.json b/package-lock.json index a5dad5f892..44193a3624 100644 --- a/package-lock.json +++ b/package-lock.json @@ -162,7 +162,7 @@ "jest-environment-node": "30.3.0", "jest-preset-angular": "17.0.0", "nx": "23.1.1", - "prettier": "3.9.6", + "prettier": "3.9.9", "prettier-plugin-organize-attributes": "1.0.0", "prisma": "7.10.0", "react": "18.2.0", @@ -30600,9 +30600,9 @@ } }, "node_modules/prettier": { - "version": "3.9.6", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", - "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index fcb94759f7..1d87566678 100644 --- a/package.json +++ b/package.json @@ -206,7 +206,7 @@ "jest-environment-node": "30.3.0", "jest-preset-angular": "17.0.0", "nx": "23.1.1", - "prettier": "3.9.6", + "prettier": "3.9.9", "prettier-plugin-organize-attributes": "1.0.0", "prisma": "7.10.0", "react": "18.2.0", From 4beba1de3def9b7e7eab616c090e52d709c34a32 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Fri, 2 Oct 2026 01:10:59 +0700 Subject: [PATCH 03/35] Task/remove deprecated undefined loading state from value component (#7964) Remove deprecated undefined loading state --- libs/ui/src/lib/value/value.component.html | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index c5ebe4e2cf..385a49ac10 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -27,11 +27,7 @@ } - - @if (isLoading() || value === undefined) { + @if (isLoading()) { =22.19.0" diff --git a/package.json b/package.json index 1d87566678..4ef3317529 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.1", - "undici": "8.10.0", + "undici": "8.11.2", "uuid": "14.0.2", "yahoo-finance2": "4.0.2", "zod": "4.6.5", From 4fc30a69119c761e194b5845a61d8148974a2611 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:30:07 +0700 Subject: [PATCH 08/35] Task/align chart data array types with LineChartItem and make groupBy optional (#7996) Align chart data array types with LineChartItem and make groupBy optional --- .../account-detail-dialog.component.ts | 18 +++++++---- .../investment-chart.component.ts | 2 +- .../analysis/analysis-page.component.ts | 32 +++++++++++-------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 44f7280473..c95dfe7bc4 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -10,7 +10,7 @@ import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { AccountBalancesResponse, Activity, - HistoricalDataItem, + LineChartItem, PortfolioPosition, User } from '@ghostfolio/common/interfaces'; @@ -99,7 +99,7 @@ export class GfAccountDetailDialogComponent implements OnInit { protected equity: number | null; protected equityPrecision = 2; protected hasPermissionToDeleteAccountBalance: boolean; - protected historicalDataItems: HistoricalDataItem[]; + protected historicalDataItems: LineChartItem[]; protected holdings: PortfolioPosition[]; protected interestInBaseCurrency: number; protected interestInBaseCurrencyPrecision = 2; @@ -364,12 +364,16 @@ export class GfAccountDetailDialogComponent implements OnInit { portfolioPerformance.chart && portfolioPerformance.chart.length > 0 ) { - this.historicalDataItems = portfolioPerformance.chart.map( - ({ date, netWorth, netWorthInPercentage }) => ({ - date, - value: isNumber(netWorth) ? netWorth : netWorthInPercentage + this.historicalDataItems = portfolioPerformance.chart + .map(({ date, netWorth, netWorthInPercentage }) => { + return { + date, + value: isNumber(netWorth) ? netWorth : netWorthInPercentage + }; }) - ); + .filter((item): item is LineChartItem => { + return isNumber(item.value); + }); } else { this.historicalDataItems = this.accountBalances.map( ({ date, valueInBaseCurrency }) => { diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index a1dc6a28f9..34500422ed 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -59,7 +59,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { @Input() public benchmarkDataLabel = ''; @Input() public colorScheme?: ColorScheme; @Input() public currency?: string; - @Input() public groupBy: GroupBy; + @Input() public groupBy?: GroupBy; @Input() public historicalDataItems: LineChartItem[] = []; @Input() public isInPercentage?: boolean = false; @Input() public isLoading = false; 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 880753cce5..a08c9d9d52 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 @@ -7,8 +7,8 @@ import { } from '@ghostfolio/common/config'; import { canOpenHoldingDetail } from '@ghostfolio/common/helper'; import { - HistoricalDataItem, InvestmentItem, + LineChartItem, PortfolioInvestmentsResponse, PortfolioPerformance, PortfolioPosition, @@ -79,7 +79,7 @@ import { forkJoin } from 'rxjs'; }) export class GfAnalysisPageComponent implements OnInit { protected benchmark?: Partial; - protected benchmarkDataItems: HistoricalDataItem[] = []; + protected benchmarkDataItems: LineChartItem[] = []; protected readonly benchmarks: Partial[]; protected bottom3: PortfolioPosition[]; protected dividendsByGroup: InvestmentItem[]; @@ -101,8 +101,8 @@ export class GfAnalysisPageComponent implements OnInit { ]; protected performance: PortfolioPerformance; protected readonly PerformanceCalculationType = PerformanceCalculationType; - protected performanceDataItems: HistoricalDataItem[]; - protected performanceDataItemsInPercentage: HistoricalDataItem[]; + protected performanceDataItems: LineChartItem[]; + protected performanceDataItemsInPercentage: LineChartItem[]; protected readonly portfolioEvolutionDataLabel = $localize`Investment`; protected precision = 2; protected savingsRatePerMonth: number | undefined; @@ -378,18 +378,24 @@ export class GfAnalysisPageComponent implements OnInit { }); } - this.performanceDataItems.push({ + const value = isNumber(valueWithCurrencyEffect) + ? valueWithCurrencyEffect + : valueInPercentage; + + if (isNumber(value)) { + this.performanceDataItems.push({ + date, + value + }); + } + } + + if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) { + this.performanceDataItemsInPercentage.push({ date, - value: isNumber(valueWithCurrencyEffect) - ? valueWithCurrencyEffect - : valueInPercentage + value: netPerformanceInPercentageWithCurrencyEffect }); } - - this.performanceDataItemsInPercentage.push({ - date, - value: netPerformanceInPercentageWithCurrencyEffect - }); } if ( From 452674eceecf5226cfb82dc70bb1d027edb85a74 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:00:34 +0200 Subject: [PATCH 09/35] Task/improve symbol validation of MCP import tool (#8000) Improve symbol validation --- .../src/app/endpoints/mcp/mcp.schemas.spec.ts | 35 ++++++++++++++++++- apps/api/src/app/endpoints/mcp/mcp.schemas.ts | 16 +++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts index 2039e7cd91..a5109d225f 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts @@ -2,7 +2,8 @@ import { DEFAULT_DATE_RANGE, MCP_MAX_ACTIVITIES, SEARCH_QUERY_MAXIMUM_LENGTH, - SEARCH_QUERY_MINIMUM_LENGTH + SEARCH_QUERY_MINIMUM_LENGTH, + SYMBOL_MAXIMUM_LENGTH } from '@ghostfolio/common/config'; import { DataSource } from '@prisma/client'; @@ -107,6 +108,38 @@ describe('IMPORT_ACTIVITIES_PARAMETERS', () => { expect(parse([createActivity({ symbol: '' })])).toBe(false); }); + it('Refuses a symbol that contains only spaces', () => { + expect(parse([createActivity({ symbol: ' ' })])).toBe(false); + }); + + it(`Accepts a symbol of ${SYMBOL_MAXIMUM_LENGTH} characters`, () => { + expect( + parse([createActivity({ symbol: 'A'.repeat(SYMBOL_MAXIMUM_LENGTH) })]) + ).toBe(true); + }); + + it(`Accepts a symbol of ${SYMBOL_MAXIMUM_LENGTH} characters with spaces at the start and the end`, () => { + expect( + parse([ + createActivity({ symbol: ` ${'A'.repeat(SYMBOL_MAXIMUM_LENGTH)} ` }) + ]) + ).toBe(true); + }); + + it(`Refuses a symbol longer than ${SYMBOL_MAXIMUM_LENGTH} characters`, () => { + expect( + parse([createActivity({ symbol: 'A'.repeat(SYMBOL_MAXIMUM_LENGTH + 1) })]) + ).toBe(false); + }); + + it('Removes spaces at the start and the end of a symbol', () => { + expect( + IMPORT_ACTIVITIES_PARAMETERS.parse({ + activities: [createActivity({ symbol: ' AAPL ' })] + }).activities[0].symbol + ).toBe('AAPL'); + }); + it('Refuses an empty identifier of an account', () => { expect(parse([createActivity({ accountId: '' })])).toBe(false); }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts index 4bcc385b35..dc075377d0 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts @@ -16,16 +16,18 @@ import { import { AssetClass, DataSource, Type as ActivityType } from '@prisma/client'; import { z } from 'zod'; +const SYMBOL_PARAMETER = z + .string() + .trim() + .min(1) + .max(SYMBOL_MAXIMUM_LENGTH) + .describe('The symbol of the asset profile'); + const HOLDING_PARAMETER = z.object({ dataSource: z .enum(DataSource) .describe('The data source of the asset profile'), - symbol: z - .string() - .trim() - .min(1) - .max(SYMBOL_MAXIMUM_LENGTH) - .describe('The symbol of the asset profile') + symbol: SYMBOL_PARAMETER }); export const GET_ACCOUNTS_PARAMETERS = z.object({ @@ -141,7 +143,7 @@ export const IMPORT_ACTIVITIES_PARAMETERS = z.object({ ), fee: z.number().min(0).describe('The fee of the activity'), quantity: z.number().min(0).describe('The quantity of the activity'), - symbol: z.string().min(1).describe('The symbol of the asset profile'), + symbol: SYMBOL_PARAMETER, type: z.enum(ActivityType).describe('The type of the activity'), unitPrice: z.number().min(0).describe('The unit price of the activity') }) From bdacd08db2dbcd17321bb157e4b3e3c468033c96 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:33:33 +0200 Subject: [PATCH 10/35] Task/improve comment validation of MCP import tool (#8004) Improve comment validation --- .../src/app/endpoints/mcp/mcp.schemas.spec.ts | 25 +++++++++++++++++++ apps/api/src/app/endpoints/mcp/mcp.schemas.ts | 8 +++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts index a5109d225f..dbc5bc698c 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts @@ -1,4 +1,5 @@ import { + COMMENT_MAXIMUM_LENGTH, DEFAULT_DATE_RANGE, MCP_MAX_ACTIVITIES, SEARCH_QUERY_MAXIMUM_LENGTH, @@ -92,6 +93,30 @@ describe('IMPORT_ACTIVITIES_PARAMETERS', () => { return IMPORT_ACTIVITIES_PARAMETERS.safeParse({ activities }).success; } + it(`Accepts a comment of ${COMMENT_MAXIMUM_LENGTH} characters with spaces at the start and the end`, () => { + expect( + parse([ + createActivity({ comment: ` ${'A'.repeat(COMMENT_MAXIMUM_LENGTH)} ` }) + ]) + ).toBe(true); + }); + + it(`Refuses a comment longer than ${COMMENT_MAXIMUM_LENGTH} characters`, () => { + expect( + parse([ + createActivity({ comment: 'A'.repeat(COMMENT_MAXIMUM_LENGTH + 1) }) + ]) + ).toBe(false); + }); + + it('Removes spaces at the start and the end of a comment', () => { + expect( + IMPORT_ACTIVITIES_PARAMETERS.parse({ + activities: [createActivity({ comment: ' note ' })] + }).activities[0].comment + ).toBe('note'); + }); + it('Refuses a currency in lower case', () => { expect(parse([createActivity({ currency: 'usd' })])).toBe(false); }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts index dc075377d0..ce66699e91 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts @@ -1,5 +1,6 @@ import { DATE_RANGE_PATTERN } from '@ghostfolio/api/dtos/date-range-filter.dto'; import { + COMMENT_MAXIMUM_LENGTH, DATE_RANGES, DEFAULT_DATE_RANGE, MCP_MAX_ACCOUNTS, @@ -124,7 +125,12 @@ export const IMPORT_ACTIVITIES_PARAMETERS = z.object({ .min(1) .optional() .describe('The identifier of the account of the activity'), - comment: z.string().optional().describe('The comment of the activity'), + comment: z + .string() + .trim() + .max(COMMENT_MAXIMUM_LENGTH) + .optional() + .describe('The comment of the activity'), currency: z .string() .refine(isValidCurrencyCode) From df935e65223ee176cefac5bb5c0b6c5888f411ef Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:36:56 +0200 Subject: [PATCH 11/35] Release 3.77.0 (#8006) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df97699608..2e0ad31db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 +## 3.77.0 - 2026-10-02 ### Added diff --git a/package-lock.json b/package-lock.json index e4562f996d..877a61cb0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.76.0", + "version": "3.77.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.76.0", + "version": "3.77.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 4ef3317529..264cf0a21b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.76.0", + "version": "3.77.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 6cba7744b572021ab42306e233bc297aa6bc5a60 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 3 Oct 2026 02:16:41 +0700 Subject: [PATCH 12/35] Task/widen input types in portfolio summary component (#8010) feat(client): widen input types in portfolio summary component --- .../portfolio-summary.component.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) 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 13686624f7..6b10d7d271 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 @@ -39,15 +39,15 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; templateUrl: './portfolio-summary.component.html' }) export class GfPortfolioSummaryComponent implements OnChanges { - @Input() baseCurrency: string; - @Input() deviceType: string; - @Input() hasImpersonationId: boolean; - @Input() hasPermissionToUpdateUserSettings: boolean; - @Input() isLoading: boolean; - @Input() language: string; - @Input() locale = getLocale(); - @Input() summary: PortfolioSummary; - @Input() user: User; + @Input() public baseCurrency?: string; + @Input() public deviceType: string; + @Input() public hasImpersonationId: boolean; + @Input() public hasPermissionToUpdateUserSettings: boolean; + @Input() public isLoading: boolean; + @Input() public language?: string; + @Input() public locale?: string = getLocale(); + @Input() public summary?: PortfolioSummary; + @Input() public user?: User; public emergencyFundChanged = output(); @@ -99,11 +99,11 @@ export class GfPortfolioSummaryComponent implements OnChanges { } protected get hasCashBreakdown() { - return !this.isLoading && this.summary?.emergencyFund?.cash > 0; + return !this.isLoading && (this.summary?.emergencyFund?.cash ?? 0) > 0; } protected get hasHoldingsBreakdown() { - return !this.isLoading && this.summary?.emergencyFund?.assets > 0; + return !this.isLoading && (this.summary?.emergencyFund?.assets ?? 0) > 0; } protected get holdingsInBaseCurrency() { @@ -133,7 +133,7 @@ export class GfPortfolioSummaryComponent implements OnChanges { } return ( - this.holdingsInBaseCurrency - (this.summary.emergencyFund?.assets ?? 0) + this.holdingsInBaseCurrency - (this.summary?.emergencyFund?.assets ?? 0) ); } @@ -200,7 +200,7 @@ export class GfPortfolioSummaryComponent implements OnChanges { this.emergencyFundChanged.emit(emergencyFund); }, confirmLabel: $localize`Save`, - defaultValue: this.summary.emergencyFund?.total?.toString() ?? '0', + defaultValue: this.summary?.emergencyFund?.total?.toString() ?? '0', title: $localize`Please set the amount of your emergency fund.`, valueSuffix: this.baseCurrency }); From 951c01c5bc1a56743bf584be6d5714c374186b2d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:33:35 +0200 Subject: [PATCH 13/35] Task/upgrade dotenv to version 18 (#8008) * Update dotenv to version 18.0.3 * Update changelog --- CHANGELOG.md | 6 ++++++ package-lock.json | 36 ++++++++++++++++++++++++++++++++---- package.json | 2 +- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0ad31db0..987e11433a 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 `dotenv` from version `17.4.2` to `18.0.3` + ## 3.77.0 - 2026-10-02 ### Added diff --git a/package-lock.json b/package-lock.json index 877a61cb0d..b01c143378 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,7 @@ "countries-list": "3.4.1", "countup.js": "2.10.1", "date-fns": "4.4.0", - "dotenv": "17.4.2", + "dotenv": "18.0.3", "dotenv-expand": "13.0.0", "envalid": "8.2.0", "express": "5.2.1", @@ -18022,6 +18022,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/c12/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/c12/node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -20710,10 +20723,13 @@ } }, "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.3.tgz", + "integrity": "sha512-/8CJTyL817qPnfbQrJgOEcrU3fDrJkRIoXBE566wHuvIXKOCHUZhPqGPbCPi9SogjdGnreF0TlvnTgK6J/8zpA==", "license": "BSD-2-Clause", + "bin": { + "dotenv": "dist/index.cjs" + }, "engines": { "node": ">=12" }, @@ -20736,6 +20752,18 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 264cf0a21b..f40bfb50cf 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "countries-list": "3.4.1", "countup.js": "2.10.1", "date-fns": "4.4.0", - "dotenv": "17.4.2", + "dotenv": "18.0.3", "dotenv-expand": "13.0.0", "envalid": "8.2.0", "express": "5.2.1", From c84fd0df8d7329598c835010f8de9443cc69e16d Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:48:52 +0200 Subject: [PATCH 14/35] Task/upgrade ai to version 7.0.114 (#8011) * Update ai to version 7.0.114 * Update changelog --- CHANGELOG.md | 2 ++ package-lock.json | 58 +++++++++++++++++++++++++++-------------------- package.json | 4 ++-- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 987e11433a..ff764bf4a8 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 +- Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` +- Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` ## 3.77.0 - 2026-10-02 diff --git a/package-lock.json b/package-lock.json index b01c143378..be96328456 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,13 +46,13 @@ "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", - "@openrouter/ai-sdk-provider": "3.0.0", + "@openrouter/ai-sdk-provider": "3.1.0", "@prisma/adapter-pg": "7.10.0", "@prisma/client": "7.10.0", "@rekog/mcp-nest": "2.0.7", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.1", - "ai": "7.0.37", + "ai": "7.0.114", "alphavantage": "2.2.0", "big.js": "7.0.1", "bootstrap": "4.6.2", @@ -189,13 +189,13 @@ "license": "MIT" }, "node_modules/@ai-sdk/gateway": { - "version": "4.0.28", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.28.tgz", - "integrity": "sha512-ee9TsNO3mkgHDWmTmJ8Fvltr6PFh52zhrV2+FaKJY3F3iu7kWZi5tCRJCR1HVhhlpj6lHniUb28nLQdPHkA/1Q==", + "version": "4.0.92", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.92.tgz", + "integrity": "sha512-awX24+QedfcIDTAdwkCUNG08vPJjkQI9KZLGh4Ua+vNbgeaNFJre96OIbBefOD9XPHSVpdXjwqtWmo4Yh7oczw==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "4.0.3", - "@ai-sdk/provider-utils": "5.0.12", + "@ai-sdk/provider": "4.0.18", + "@ai-sdk/provider-utils": "5.0.47", "@vercel/oidc": "3.2.0" }, "engines": { @@ -206,9 +206,9 @@ } }, "node_modules/@ai-sdk/provider": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.3.tgz", - "integrity": "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==", + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.18.tgz", + "integrity": "sha512-+GZJIgz1jk86pwEbb3f1BD2bdoSKyWE4Jg4YUc7NMnMozbWemSKYZcw2F4nMaO5qwsL5A8RmioAKW81YktRpIQ==", "license": "Apache-2.0", "dependencies": { "json-schema": "^0.4.0" @@ -218,15 +218,16 @@ } }, "node_modules/@ai-sdk/provider-utils": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.12.tgz", - "integrity": "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==", + "version": "5.0.47", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.47.tgz", + "integrity": "sha512-NZQ1hMxY6Hd4ZiinjpU5HUOsyCHiTFkHw7AiM1EVN+Cd3kNTi47flZWNvFwGqnBojntB1+2eBRTeHRuVLtPhEg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "4.0.3", + "@ai-sdk/provider": "4.0.18", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", - "eventsource-parser": "^3.0.8" + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" }, "engines": { "node": ">=22" @@ -235,6 +236,15 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/provider-utils/node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -11951,9 +11961,9 @@ } }, "node_modules/@openrouter/ai-sdk-provider": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-3.0.0.tgz", - "integrity": "sha512-m9XTSWoODH2RM5OsZpaGiN7QRR8cdP5paBWq699Tu3JVmGPBKT8xF8XwV0ZBVVsjikD/JgWfak4VSsTR4wAVbg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@openrouter/ai-sdk-provider/-/ai-sdk-provider-3.1.0.tgz", + "integrity": "sha512-BQy1TA9fKrh47s9ivsO6FY5QimN7GUF0i8Qon3pkxQclEgShso0dClpcWn8OIyPLEZZPFaFKgI8TW4S/O0p0Dw==", "license": "Apache-2.0", "engines": { "node": ">=22" @@ -16840,14 +16850,14 @@ } }, "node_modules/ai": { - "version": "7.0.37", - "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.37.tgz", - "integrity": "sha512-stF+SEQJgKY3Qfe3FwNzqUrehHviOp2l7LemoI8YMOa0Zk5PxKsRNgUk3cHXz0RAue9RRyCAis2fz6LSCP6EKw==", + "version": "7.0.114", + "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.114.tgz", + "integrity": "sha512-jRMOw7tPMWVYomoHWY5xHBXPQDyKTY1Pn3BX0Rh39UOP6y9FYq6xCIB6khgIC/b9CRg4+IxadwIbie+Eu7yU3g==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "4.0.28", - "@ai-sdk/provider": "4.0.3", - "@ai-sdk/provider-utils": "5.0.12" + "@ai-sdk/gateway": "4.0.92", + "@ai-sdk/provider": "4.0.18", + "@ai-sdk/provider-utils": "5.0.47" }, "engines": { "node": ">=22" diff --git a/package.json b/package.json index f40bfb50cf..7a32e9bdf5 100644 --- a/package.json +++ b/package.json @@ -90,13 +90,13 @@ "@nestjs/schedule": "6.1.3", "@nestjs/serve-static": "5.0.5", "@nestjs/throttler": "6.5.0", - "@openrouter/ai-sdk-provider": "3.0.0", + "@openrouter/ai-sdk-provider": "3.1.0", "@prisma/adapter-pg": "7.10.0", "@prisma/client": "7.10.0", "@rekog/mcp-nest": "2.0.7", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.1", - "ai": "7.0.37", + "ai": "7.0.114", "alphavantage": "2.2.0", "big.js": "7.0.1", "bootstrap": "4.6.2", From beb8f9de78950f373375193bb090ee9587d70881 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:13:08 +0200 Subject: [PATCH 15/35] Bugfix/carry forward of market prices between chart dates (#8009) * Carry forward of market prices between chart dates * Update changelog --- CHANGELOG.md | 4 + .../calculator/portfolio-calculator.ts | 33 ++-- ...h-market-price-between-chart-dates.spec.ts | 170 ++++++++++++++++++ .../portfolio/current-rate.service.mock.ts | 14 ++ 4 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-with-market-price-between-chart-dates.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ff764bf4a8..0d3af3346e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` +### Fixed + +- Fixed the portfolio calculation for holdings with historical market prices between the chart dates + ## 3.77.0 - 2026-10-02 ### Added diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index e03b530258..eb1e6f9fac 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -791,6 +791,16 @@ export abstract class PortfolioCalculator { activitiesByDate[activity.date].push(activity); } + // Carry forward the market prices of all dates, not only of the chart + // dates, as sparse market data (e.g. MANUAL) can be between two chart dates + const marketPriceDates = Object.keys(marketSymbolMap) + .filter((date) => { + return !!marketSymbolMap[date][assetProfileIdentifier]; + }) + .sort(); + + let indexOfMarketPriceDate = 0; + for (const dateString of chartDates) { if (dateString < startDateString) { continue; @@ -798,6 +808,18 @@ export abstract class PortfolioCalculator { break; } + while ( + indexOfMarketPriceDate < marketPriceDates.length && + marketPriceDates[indexOfMarketPriceDate] <= dateString + ) { + lastMarketPrice = + marketSymbolMap[marketPriceDates[indexOfMarketPriceDate]][ + assetProfileIdentifier + ]; + + indexOfMarketPriceDate += 1; + } + const activitiesOfDate = activitiesByDate[dateString]; if (!lastMarketPrice && activitiesOfDate?.length > 0) { @@ -808,13 +830,8 @@ export abstract class PortfolioCalculator { } } - const marketPrice = marketSymbolMap[dateString]?.[assetProfileIdentifier]; - const unitPrice = - marketPrice ?? - lastMarketPrice ?? - lastActivityUnitPrice ?? - unitPriceAtEndDate; + lastMarketPrice ?? lastActivityUnitPrice ?? unitPriceAtEndDate; if (activitiesOfDate?.length > 0) { for (const activity of activitiesOfDate) { @@ -832,10 +849,6 @@ export abstract class PortfolioCalculator { unitPriceFromMarketData: unitPrice }); } - - if (marketPrice) { - lastMarketPrice = marketPrice; - } } // Sort the activities so that the start and end placeholder activities diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-with-market-price-between-chart-dates.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-with-market-price-between-chart-dates.spec.ts new file mode 100644 index 0000000000..989409e77b --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-manual-buy-with-market-price-between-chart-dates.spec.ts @@ -0,0 +1,170 @@ +import { + activityDummyData, + assetProfileDummyData, + 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 { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +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 { parseDate } from '@ghostfolio/common/helper'; +import { Activity } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeEach(() => { + PortfolioSnapshotServiceMock.reset(); + RedisCacheServiceMock.reset(); + + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it('with MANUAL buy and market price between two chart dates', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2024-01-31').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'USD', + dataSource: 'MANUAL', + name: 'Private Investment', + symbol: '6c0c5cee-0208-4975-b473-03baf2518497' + }, + date: parseDate('2021-01-04'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 10, + type: 'BUY', + unitPriceInAssetProfileCurrency: 50 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const historicalDataByDate = Object.fromEntries( + portfolioSnapshot.historicalData.map((historicalDataItem) => { + return [historicalDataItem.date, historicalDataItem]; + }) + ); + + /** + * The only historical market prices are on 2023-06-14 and 2023-06-15, + * which are not chart dates (every third day from 2021-01-03) + */ + expect(historicalDataByDate['2023-06-14']).toBeUndefined(); + expect(historicalDataByDate['2023-06-15']).toBeUndefined(); + + /** + * The chart dates before the market prices use the unit price of the + * activity: 50 + */ + expect(historicalDataByDate['2023-06-13']).toMatchObject({ + netPerformance: 0, // 10 * (50 - 50) = 0 + totalInvestment: 500, + value: 500 // 10 * 50 = 500 + }); + + /** + * The chart dates after the market prices carry the latest one forward + * (100), not the first one (80) + */ + expect(historicalDataByDate['2023-06-16']).toMatchObject({ + netPerformance: 500, // 10 * (100 - 50) = 500 + totalInvestment: 500, + value: 1000 // 10 * 100 = 1000 + }); + + expect(historicalDataByDate['2024-01-30']).toMatchObject({ + netPerformance: 500, // 10 * (100 - 50) = 500 + totalInvestment: 500, + value: 1000 // 10 * 100 = 1000 + }); + + /** + * The market price is unchanged since 2023-06-15, hence there is no + * performance today + */ + expect(portfolioSnapshot.positions[0]).toMatchObject({ + netPerformancePercentageWithCurrencyEffectMap: { + '1d': new Big(0) + }, + netPerformanceWithCurrencyEffectMap: { + '1d': new Big(0), + max: new Big(500) // 10 * (100 - 50) = 500 + } + }); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 7fcacad610..18739ddab5 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -30,6 +30,20 @@ function mockGetValue({ return { marketPrice: 0 }; + case '6c0c5cee-0208-4975-b473-03baf2518497': + // Sparse market data with the quote of today, which falls back to the + // latest market price + if (isSameDay(parseDate('2023-06-14'), date)) { + return { marketPrice: 80 }; + } else if ( + isSameDay(parseDate('2023-06-15'), date) || + isSameDay(parseDate('2024-01-31'), date) + ) { + return { marketPrice: 100 }; + } + + return { marketPrice: 0 }; + case 'BALN.SW': if (isSameDay(parseDate('2021-11-12'), date)) { return { marketPrice: 146 }; From acd14c6298f1e18d3d17d57ea94d87b23e3748e8 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:14:54 +0200 Subject: [PATCH 16/35] Task/improve label of cash positions (#8007) * Improve label of cash positions * Update changelog --- CHANGELOG.md | 1 + .../allocations/allocations-page.component.ts | 3 +- .../app/pages/public/public-page.component.ts | 3 +- libs/common/src/lib/helper.spec.ts | 47 +++++++++++++++++++ libs/common/src/lib/helper.ts | 16 +++++++ .../holdings-table.component.html | 13 +++-- .../holdings-table.component.ts | 12 ++++- .../treemap-chart/treemap-chart.component.ts | 14 +++--- 8 files changed, 96 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d3af3346e..959f6f09ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the label of the cash positions in the holdings charts and table - Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` - Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index cbdacaa3f1..915c6833ab 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -11,6 +11,7 @@ import { convertValuesToPercentagesOfTotal, getAssetProfileIdentifier, getCountryName, + getHoldingName, isCashPosition } from '@ghostfolio/common/helper'; import { @@ -533,7 +534,7 @@ export class GfAllocationsPageComponent implements OnInit { value, dataSource: position.assetProfile.dataSource, isClickable: canOpenHoldingDetail(position), - name: position.assetProfile.name ?? '' + name: getHoldingName(position.assetProfile) }; } } diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index 46606753e4..8fa8abde18 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -3,6 +3,7 @@ import { convertValuesToPercentagesOfTotal, getAssetProfileIdentifier, getCountryName, + getHoldingName, isCashPosition } from '@ghostfolio/common/helper'; import { @@ -260,7 +261,7 @@ export class GfPublicPageComponent implements OnInit { this.symbols[symbol] = { symbol, value, - name: position.assetProfile.name ?? symbol + name: getHoldingName(position.assetProfile) }; } } diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index b7e7635670..71b934628c 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -7,6 +7,7 @@ import { extractNumberFromString, getCountryCodeFromCurrency, getEmojiFlag, + getHoldingName, getNumberFormatGroup, getStringOrNull, getStringOrUndefined, @@ -143,6 +144,52 @@ describe('Helper', () => { }); }); + describe('Get holding name', () => { + it('Cash position', () => { + expect( + getHoldingName({ + assetSubClass: 'CASH', + assetSubClassLabel: 'Cash', + name: 'CHF', + symbol: 'CHF' + }) + ).toEqual('Cash (CHF)'); + }); + + it('Cash position without asset sub class label', () => { + expect( + getHoldingName({ + assetSubClass: 'CASH', + assetSubClassLabel: undefined, + name: 'CHF', + symbol: 'CHF' + }) + ).toEqual('CHF'); + }); + + it('Holding with name', () => { + expect( + getHoldingName({ + assetSubClass: 'STOCK', + assetSubClassLabel: 'Stock', + name: 'Apple Inc.', + symbol: 'AAPL' + }) + ).toEqual('Apple Inc.'); + }); + + it('Holding without name', () => { + expect( + getHoldingName({ + assetSubClass: 'STOCK', + assetSubClassLabel: 'Stock', + name: undefined, + symbol: 'AAPL' + }) + ).toEqual('AAPL'); + }); + }); + describe('Get number format group', () => { let languageGetter: jest.SpyInstance; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 2a76261dae..7e4e13de7e 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -453,6 +453,22 @@ export function getEmojiFlag(aCountryCode: string) { }); } +export function getHoldingName({ + assetSubClass, + assetSubClassLabel, + name, + symbol +}: Pick< + PortfolioPosition['assetProfile'], + 'assetSubClass' | 'assetSubClassLabel' | 'name' | 'symbol' +>) { + if (isCashPosition({ assetSubClass }) && assetSubClassLabel) { + return `${assetSubClassLabel} (${symbol})`; + } + + return name ?? symbol; +} + export function getLocale() { return navigator.language ?? DEFAULT_LOCALE; } diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html index 2001a1b990..e1c012ab15 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.html +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -20,7 +20,7 @@ [countryCode]="getCountryCodeForCashPosition(element)" [dataSource]="element.assetProfile.dataSource" [symbol]="element.assetProfile.symbol" - [tooltip]="element.assetProfile.name" + [tooltip]="getHoldingName(element.assetProfile)" /> @@ -40,9 +40,14 @@ mat-cell [class.w-100]="mode() === 'simple'" > -
- {{ element.assetProfile.name }} - @if (element.assetProfile.name === element.assetProfile.symbol) { + @let holdingName = getHoldingName(element.assetProfile); + +
+ {{ holdingName }} + @if ( + !isCashPosition(element.assetProfile) && + element.assetProfile.name === element.assetProfile.symbol + ) { ({{ element.assetProfile.assetSubClassLabel }}) }
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts index eac1c9d918..1894260b78 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts @@ -1,6 +1,7 @@ import { canOpenHoldingDetail, getCountryCodeFromCurrency, + getHoldingName, getLocale, getLowercase, isCashPosition @@ -89,6 +90,9 @@ export class GfHoldingsTableComponent { return columns; }); + protected readonly getHoldingName = getHoldingName; + protected readonly isCashPosition = isCashPosition; + protected readonly isLoading = computed(() => { return !this.holdings(); }); @@ -104,7 +108,13 @@ export class GfHoldingsTableComponent { }); public constructor() { - this.dataSource.sortingDataAccessor = getLowercase; + this.dataSource.sortingDataAccessor = (holding, path) => { + if (path === 'assetProfile.name') { + return getHoldingName(holding.assetProfile).toLocaleLowerCase(); + } + + return getLowercase(holding, path) as number | string; + }; // Reactive data update effect(() => { diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 83b9916c6e..01037f77f4 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -3,7 +3,11 @@ import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; -import { canOpenHoldingDetail, getLocale } from '@ghostfolio/common/helper'; +import { + canOpenHoldingDetail, + getHoldingName, + getLocale +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, PortfolioPosition @@ -399,20 +403,18 @@ export class GfTreemapChartComponent const raw = context.raw as GfTreemapDataPoint; const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`; - const name = raw._data.assetProfile.name; + const name = getHoldingName(raw._data.assetProfile); const sign = raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; - const symbol = raw._data.assetProfile.symbol; - const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; if (raw._data.valueInBaseCurrency !== null) { const value = raw._data.valueInBaseCurrency; return [ - `${name ?? symbol} (${allocationInPercentage})`, + `${name} (${allocationInPercentage})`, `${value?.toLocaleString(this.locale(), { maximumFractionDigits: 2, minimumFractionDigits: 2 @@ -429,7 +431,7 @@ export class GfTreemapChartComponent ]; } else { return [ - `${name ?? symbol} (${allocationInPercentage})`, + `${name} (${allocationInPercentage})`, '', $localize`Performance`, netPerformanceInPercentageWithSign From f260f1572ecaf971ff3a4ca530d08510bb5ecf88 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:15:30 +0200 Subject: [PATCH 17/35] Task/improve symbol validation of create and update order DTO (#8005) Improve symbol validation --- libs/common/src/lib/dtos/create-order.dto.ts | 5 +++++ libs/common/src/lib/dtos/update-order.dto.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/libs/common/src/lib/dtos/create-order.dto.ts b/libs/common/src/lib/dtos/create-order.dto.ts index d473e8e7b6..d2374e9b1a 100644 --- a/libs/common/src/lib/dtos/create-order.dto.ts +++ b/libs/common/src/lib/dtos/create-order.dto.ts @@ -18,6 +18,7 @@ import { IsBoolean, IsEnum, IsISO8601, + IsNotEmpty, IsNumber, IsOptional, IsString, @@ -71,8 +72,12 @@ export class CreateOrderDto { @Min(0) quantity: number; + @IsNotEmpty() @IsString() @MaxLength(SYMBOL_MAXIMUM_LENGTH) + @Transform(({ value }: TransformFnParams) => + isString(value) ? value.trim() : value + ) symbol: string; @ArrayUnique() diff --git a/libs/common/src/lib/dtos/update-order.dto.ts b/libs/common/src/lib/dtos/update-order.dto.ts index f7dfb0df53..77e4a42286 100644 --- a/libs/common/src/lib/dtos/update-order.dto.ts +++ b/libs/common/src/lib/dtos/update-order.dto.ts @@ -17,6 +17,7 @@ import { IsArray, IsEnum, IsISO8601, + IsNotEmpty, IsNumber, IsOptional, IsString, @@ -72,8 +73,12 @@ export class UpdateOrderDto { @Min(0) quantity: number; + @IsNotEmpty() @IsString() @MaxLength(SYMBOL_MAXIMUM_LENGTH) + @Transform(({ value }: TransformFnParams) => + isString(value) ? value.trim() : value + ) symbol: string; @ArrayUnique() From f987e4160ed80c39d552f9eb9c94f783fe3fb92b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:16:07 +0200 Subject: [PATCH 18/35] Task/improve symbol validation of create asset profile DTO (#8013) Improve symbol validation --- libs/common/src/lib/dtos/create-asset-profile.dto.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libs/common/src/lib/dtos/create-asset-profile.dto.ts b/libs/common/src/lib/dtos/create-asset-profile.dto.ts index b4fe347071..5d75b39a1d 100644 --- a/libs/common/src/lib/dtos/create-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/create-asset-profile.dto.ts @@ -10,17 +10,19 @@ import { DataSource } from '@ghostfolio/prisma/enums'; -import { Type } from 'class-transformer'; +import { Transform, TransformFnParams, Type } from 'class-transformer'; import { IsArray, IsBoolean, IsEnum, + IsNotEmpty, IsOptional, IsString, IsUrl, MaxLength, ValidateNested } from 'class-validator'; +import { isString } from 'lodash-es'; import { CountryDto } from './country.dto'; import { HoldingDto } from './holding.dto'; @@ -92,8 +94,12 @@ export class CreateAssetProfileDto { @ValidateNested({ each: true }) sectors?: Prisma.InputJsonArray; + @IsNotEmpty() @IsString() @MaxLength(SYMBOL_MAXIMUM_LENGTH) + @Transform(({ value }: TransformFnParams) => + isString(value) ? value.trim() : value + ) symbol: string; @IsOptional() From 2364489ee4dd9b74df2379858d25f4a49a96852a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:59:37 +0200 Subject: [PATCH 19/35] Feature/data source in MCP tools (#8019) * Extend tools with data source * Update changelog --- CHANGELOG.md | 1 + .../src/app/endpoints/mcp/mcp.schemas.spec.ts | 89 ++++++++ apps/api/src/app/endpoints/mcp/mcp.schemas.ts | 24 ++- .../src/app/endpoints/mcp/mcp.service.spec.ts | 114 +++++++++- apps/api/src/app/endpoints/mcp/mcp.service.ts | 31 +-- .../api/src/helper/data-source.helper.spec.ts | 123 +++++++++++ apps/api/src/helper/data-source.helper.ts | 65 ++++++ ...orm-data-source-in-response.interceptor.ts | 5 +- .../data-source-table-context.interface.ts | 5 + .../holdings-table-context.interface.ts | 4 +- .../portfolio-table/portfolio-table.module.ts | 9 +- .../portfolio-table.service.spec.ts | 196 +++++++++++++++++- .../portfolio-table.service.ts | 142 +++++++++---- 13 files changed, 720 insertions(+), 88 deletions(-) create mode 100644 apps/api/src/helper/data-source.helper.spec.ts create mode 100644 apps/api/src/services/portfolio-table/interfaces/data-source-table-context.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 959f6f09ef..d64f3d12f3 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 label of the cash positions in the holdings charts and table +- Extended the tools to get the activities, the portfolio and the watchlist in the server of the Model Context Protocol (MCP) to include the data source (experimental) - Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` - Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts index dbc5bc698c..e4893c2a7b 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts @@ -1,3 +1,4 @@ +import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper'; import { COMMENT_MAXIMUM_LENGTH, DEFAULT_DATE_RANGE, @@ -18,6 +19,8 @@ import { } from './mcp.schemas'; import { createActivity } from './mcp.test-utils'; +const UNKNOWN_ENCODED_DATA_SOURCE = 'ffffffff'; + describe('GET_ACCOUNTS_PARAMETERS', () => { it('Refuses an empty symbol of the holding', () => { expect( @@ -34,6 +37,40 @@ describe('GET_ACCOUNTS_PARAMETERS', () => { }).success ).toBe(false); }); + + it('Accepts an encoded data source of the holding', () => { + expect( + GET_ACCOUNTS_PARAMETERS.safeParse({ + holding: { + dataSource: encodeDataSource(DataSource.YAHOO), + symbol: 'AAPL' + } + }).success + ).toBe(true); + }); + + it('Refuses an unknown encoded data source of the holding', () => { + expect( + GET_ACCOUNTS_PARAMETERS.safeParse({ + holding: { dataSource: UNKNOWN_ENCODED_DATA_SOURCE, symbol: 'AAPL' } + }).success + ).toBe(false); + }); + + it('Gives the valid data sources in the error of an unknown data source of the holding', () => { + for (const dataSource of ['yahoo', 1]) { + const { error } = GET_ACCOUNTS_PARAMETERS.safeParse({ + holding: { dataSource, symbol: 'AAPL' } + }); + + expect(error.issues).toEqual([ + expect.objectContaining({ + message: expect.stringContaining(`|${DataSource.YAHOO}`), + path: ['holding', 'dataSource'] + }) + ]); + } + }); }); describe('GET_ACTIVITIES_PARAMETERS', () => { @@ -52,6 +89,25 @@ describe('GET_ACTIVITIES_PARAMETERS', () => { }).success ).toBe(false); }); + + it('Accepts an encoded data source of the holding', () => { + expect( + GET_ACTIVITIES_PARAMETERS.safeParse({ + holding: { + dataSource: encodeDataSource(DataSource.YAHOO), + symbol: 'AAPL' + } + }).success + ).toBe(true); + }); + + it('Refuses an unknown encoded data source of the holding', () => { + expect( + GET_ACTIVITIES_PARAMETERS.safeParse({ + holding: { dataSource: UNKNOWN_ENCODED_DATA_SOURCE, symbol: 'AAPL' } + }).success + ).toBe(false); + }); }); describe('GET_PERFORMANCE_PARAMETERS', () => { @@ -86,6 +142,25 @@ describe('GET_PERFORMANCE_PARAMETERS', () => { }).success ).toBe(false); }); + + it('Accepts an encoded data source of the holding', () => { + expect( + GET_PERFORMANCE_PARAMETERS.safeParse({ + holding: { + dataSource: encodeDataSource(DataSource.YAHOO), + symbol: 'AAPL' + } + }).success + ).toBe(true); + }); + + it('Refuses an unknown encoded data source of the holding', () => { + expect( + GET_PERFORMANCE_PARAMETERS.safeParse({ + holding: { dataSource: UNKNOWN_ENCODED_DATA_SOURCE, symbol: 'AAPL' } + }).success + ).toBe(false); + }); }); describe('IMPORT_ACTIVITIES_PARAMETERS', () => { @@ -125,6 +200,20 @@ describe('IMPORT_ACTIVITIES_PARAMETERS', () => { expect(parse([createActivity({ currency: 'USD' })])).toBe(true); }); + it('Accepts an encoded data source', () => { + expect( + parse([ + createActivity({ dataSource: encodeDataSource(DataSource.YAHOO) }) + ]) + ).toBe(true); + }); + + it('Refuses an unknown encoded data source', () => { + expect( + parse([createActivity({ dataSource: UNKNOWN_ENCODED_DATA_SOURCE })]) + ).toBe(false); + }); + it('Refuses a date at or before the epoch', () => { expect(parse([createActivity({ date: '0000-01-01' })])).toBe(false); }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts index ce66699e91..1b200e0119 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts @@ -1,4 +1,5 @@ import { DATE_RANGE_PATTERN } from '@ghostfolio/api/dtos/date-range-filter.dto'; +import { isValidEncodedDataSource } from '@ghostfolio/api/helper/data-source.helper'; import { COMMENT_MAXIMUM_LENGTH, DATE_RANGES, @@ -17,6 +18,20 @@ import { import { AssetClass, DataSource, Type as ActivityType } from '@prisma/client'; import { z } from 'zod'; +const DATA_SOURCE_PARAMETER_ERROR = `Invalid option: expected one of ${Object.values(DataSource).join('|')} or a data source as given by a tool`; + +const DATA_SOURCE_PARAMETER = z + .union( + [ + z.enum(DataSource), + z.string().refine(isValidEncodedDataSource, { + error: DATA_SOURCE_PARAMETER_ERROR + }) + ], + { error: DATA_SOURCE_PARAMETER_ERROR } + ) + .describe('The data source of the asset profile'); + const SYMBOL_PARAMETER = z .string() .trim() @@ -25,9 +40,7 @@ const SYMBOL_PARAMETER = z .describe('The symbol of the asset profile'); const HOLDING_PARAMETER = z.object({ - dataSource: z - .enum(DataSource) - .describe('The data source of the asset profile'), + dataSource: DATA_SOURCE_PARAMETER, symbol: SYMBOL_PARAMETER }); @@ -137,10 +150,7 @@ export const IMPORT_ACTIVITIES_PARAMETERS = z.object({ .describe( 'The currency of the fee and of the unit price, as an ISO 4217 code in upper case' ), - dataSource: z - .enum(DataSource) - .optional() - .describe('The data source of the asset profile'), + dataSource: DATA_SOURCE_PARAMETER.optional(), date: z .string() .refine(isValidDateAfter1970) diff --git a/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts b/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts index f4841896bd..a013341179 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts @@ -2,6 +2,7 @@ import { ImportValidationError } from '@ghostfolio/api/app/import/errors/import- import { ImportService } from '@ghostfolio/api/app/import/import.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { PortfolioTableService } from '@ghostfolio/api/services/portfolio-table/portfolio-table.service'; @@ -128,6 +129,22 @@ describe('McpService', () => { ); }); + it('Decodes an encoded data source', async () => { + configuration.ENABLE_FEATURE_SUBSCRIPTION = true; + + await mcpService.getAccounts({ + userId, + holding: { + dataSource: encodeDataSource(DataSource.YAHOO), + symbol: 'AAPL' + } + }); + + expect(apiService.buildFiltersFromQueryParams).toHaveBeenCalledWith( + expect.objectContaining({ filterByDataSource: DataSource.YAHOO }) + ); + }); + it('Keeps the data source if the subscription is not enabled', async () => { configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; configuration.ENABLE_FEATURE_SUBSCRIPTION = false; @@ -192,6 +209,21 @@ describe('McpService', () => { ); }); + it('Decodes an encoded data source', async () => { + configuration.ENABLE_FEATURE_SUBSCRIPTION = true; + + await getActivities({ + holding: { + dataSource: encodeDataSource(DataSource.YAHOO), + symbol: 'AAPL' + } + }); + + expect(apiService.buildFiltersFromQueryParams).toHaveBeenCalledWith( + expect.objectContaining({ filterByDataSource: DataSource.YAHOO }) + ); + }); + it('Keeps the data source if the subscription is not enabled', async () => { configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; configuration.ENABLE_FEATURE_SUBSCRIPTION = false; @@ -275,6 +307,23 @@ describe('McpService', () => { ); }); + it('Decodes an encoded data source', async () => { + configuration.ENABLE_FEATURE_SUBSCRIPTION = true; + + await mcpService.getPerformance({ + userId, + holding: { + dataSource: encodeDataSource(DataSource.YAHOO), + symbol: 'AAPL' + }, + range: 'max' + }); + + expect(apiService.buildFiltersFromQueryParams).toHaveBeenCalledWith( + expect.objectContaining({ filterByDataSource: DataSource.YAHOO }) + ); + }); + it('Keeps the data source if the subscription is not enabled', async () => { configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; configuration.ENABLE_FEATURE_SUBSCRIPTION = false; @@ -304,14 +353,15 @@ describe('McpService', () => { }); describe('getPortfolio', () => { - it('Gives the table of the holdings in the default language', async () => { + it('Gives the table of the holdings with the data source in the default language', async () => { expect(await mcpService.getPortfolio({ userId })).toEqual({ content: [{ text: '## Holdings', type: 'text' }] }); expect(portfolioTableService.getHoldingsTable).toHaveBeenCalledWith({ userId, - languageCode: DEFAULT_LANGUAGE_CODE + languageCode: DEFAULT_LANGUAGE_CODE, + withDataSource: true }); }); }); @@ -342,9 +392,6 @@ describe('McpService', () => { it('Gives the import-ready asset profiles available to the user', async () => { const user = setupUser([permissions.createActivity]); - configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; - configuration.ENABLE_FEATURE_SUBSCRIPTION = true; - jest.spyOn(symbolService, 'lookup').mockResolvedValue({ items: [ { @@ -383,12 +430,46 @@ describe('McpService', () => { assetClass: AssetClass.EQUITY, assetSubClass: AssetSubClass.STOCK, currency: 'USD', - dataSource: DataSource.GHOSTFOLIO, + dataSource: DataSource.YAHOO, + name: 'Apple Inc.', + symbol: 'AAPL' + } + ] + }); + }); + + it('Encodes the data source if the subscription is enabled', async () => { + setupUser([permissions.createActivity]); + + configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; + configuration.ENABLE_FEATURE_SUBSCRIPTION = true; + + jest.spyOn(symbolService, 'lookup').mockResolvedValue({ + items: [ + { + assetClass: AssetClass.EQUITY, + assetSubClass: AssetSubClass.STOCK, + currency: 'USD', + dataProviderInfo: { isPremium: false }, + dataSource: DataSource.YAHOO, name: 'Apple Inc.', symbol: 'AAPL' } ] }); + + const result = await mcpService.searchAssetProfiles({ + query: 'Apple', + userId + }); + + expect(JSON.parse(result.content[0].text)).toEqual({ + assetProfiles: [ + expect.objectContaining({ + dataSource: encodeDataSource(DataSource.YAHOO) + }) + ] + }); }); }); @@ -448,6 +529,27 @@ describe('McpService', () => { ); }); + it('Decodes an encoded data source', async () => { + setupUser([permissions.createActivity]); + + configuration.ENABLE_FEATURE_SUBSCRIPTION = true; + + await mcpService.importActivities({ + userId, + activities: [ + createActivity({ dataSource: encodeDataSource(DataSource.YAHOO) }) + ] + }); + + expect(importService.import).toHaveBeenCalledWith( + expect.objectContaining({ + activitiesDto: [ + expect.objectContaining({ dataSource: DataSource.YAHOO }) + ] + }) + ); + }); + it('Keeps the data source if the subscription is not enabled', async () => { setupUser([permissions.createActivity]); diff --git a/apps/api/src/app/endpoints/mcp/mcp.service.ts b/apps/api/src/app/endpoints/mcp/mcp.service.ts index 6a989dc856..70be380060 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.service.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.service.ts @@ -2,8 +2,8 @@ import { ImportService } from '@ghostfolio/api/app/import/import.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { - getMaskedGhostfolioDataSource, - getUnmaskedGhostfolioDataSource + transformDataSourceInRequest, + transformDataSourceInResponse } from '@ghostfolio/api/helper/data-source.helper'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -121,7 +121,8 @@ export class McpService { public async getPortfolio({ userId }: { userId: string }) { const table = await this.portfolioTableService.getHoldingsTable({ userId, - languageCode: DEFAULT_LANGUAGE_CODE + languageCode: DEFAULT_LANGUAGE_CODE, + withDataSource: true }); return this.getTextResult(table); @@ -144,13 +145,11 @@ export class McpService { permission: permissions.createActivity }); - const ghostfolioDataSources = this.getGhostfolioDataSources(); - const activitiesDto = activities.map((activity) => { return { ...activity, - dataSource: getUnmaskedGhostfolioDataSource({ - ghostfolioDataSources, + dataSource: transformDataSourceInRequest({ + configurationService: this.configurationService, dataSource: activity.dataSource }) }; @@ -188,8 +187,6 @@ export class McpService { const { items } = await this.symbolService.lookup({ query, user }); - const ghostfolioDataSources = this.getGhostfolioDataSources(); - const assetProfiles = items.flatMap( ({ assetClass, @@ -211,9 +208,9 @@ export class McpService { currency, name, symbol, - dataSource: getMaskedGhostfolioDataSource({ + dataSource: transformDataSourceInResponse({ dataSource, - ghostfolioDataSources + configurationService: this.configurationService }) } ]; @@ -223,19 +220,13 @@ export class McpService { return this.getTextResult(JSON.stringify({ assetProfiles }, null, 2)); } - private getGhostfolioDataSources() { - return this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') - ? this.configurationService.get('DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER') - : []; - } - private getHoldingFilterParameters({ holding }: Pick, 'holding'>) { return { - filterByDataSource: getUnmaskedGhostfolioDataSource({ - dataSource: holding?.dataSource, - ghostfolioDataSources: this.getGhostfolioDataSources() + filterByDataSource: transformDataSourceInRequest({ + configurationService: this.configurationService, + dataSource: holding?.dataSource }), filterBySymbol: holding?.symbol }; diff --git a/apps/api/src/helper/data-source.helper.spec.ts b/apps/api/src/helper/data-source.helper.spec.ts new file mode 100644 index 0000000000..02d979441e --- /dev/null +++ b/apps/api/src/helper/data-source.helper.spec.ts @@ -0,0 +1,123 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; + +import { DataSource } from '@prisma/client'; + +import { + encodeDataSource, + isValidEncodedDataSource, + transformDataSourceInRequest, + transformDataSourceInResponse +} from './data-source.helper'; + +function createConfigurationService({ + isSubscriptionEnabled +}: { + isSubscriptionEnabled: boolean; +}) { + const configuration: Record = { + DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: [DataSource.YAHOO], + ENABLE_FEATURE_SUBSCRIPTION: isSubscriptionEnabled + }; + + return { + get: jest.fn((key: string) => { + return configuration[key]; + }) + } as unknown as ConfigurationService; +} + +describe('DataSourceHelper', () => { + describe('isValidEncodedDataSource', () => { + it('Accepts an encoded data source', () => { + expect(isValidEncodedDataSource(encodeDataSource(DataSource.YAHOO))).toBe( + true + ); + }); + + it('Refuses a data source which is not encoded', () => { + expect(isValidEncodedDataSource(DataSource.YAHOO)).toBe(false); + }); + + it('Refuses an unknown encoded data source', () => { + expect(isValidEncodedDataSource('ffffffff')).toBe(false); + }); + }); + + describe('transformDataSourceInRequest', () => { + const configurationService = createConfigurationService({ + isSubscriptionEnabled: true + }); + + it('Decodes an encoded data source', () => { + expect( + transformDataSourceInRequest({ + configurationService, + dataSource: encodeDataSource(DataSource.COINGECKO) + }) + ).toBe(DataSource.COINGECKO); + }); + + it('Resolves the mask of the data source of the Ghostfolio data provider', () => { + expect( + transformDataSourceInRequest({ + configurationService, + dataSource: DataSource.GHOSTFOLIO + }) + ).toBe(DataSource.YAHOO); + }); + + it('Keeps a data source which is not encoded', () => { + expect( + transformDataSourceInRequest({ + configurationService, + dataSource: DataSource.COINGECKO + }) + ).toBe(DataSource.COINGECKO); + }); + + it('Keeps an absent data source', () => { + expect( + transformDataSourceInRequest({ + configurationService, + dataSource: undefined + }) + ).toBeUndefined(); + }); + }); + + describe('transformDataSourceInResponse', () => { + it('Encodes the data source if the subscription is enabled', () => { + expect( + transformDataSourceInResponse({ + configurationService: createConfigurationService({ + isSubscriptionEnabled: true + }), + dataSource: DataSource.YAHOO + }) + ).toBe(encodeDataSource(DataSource.YAHOO)); + }); + + it('Keeps the data sources GHOSTFOLIO and MANUAL if the subscription is enabled', () => { + const configurationService = createConfigurationService({ + isSubscriptionEnabled: true + }); + + for (const dataSource of [DataSource.GHOSTFOLIO, DataSource.MANUAL]) { + expect( + transformDataSourceInResponse({ configurationService, dataSource }) + ).toBe(dataSource); + } + }); + + it('Keeps the data source if the subscription is not enabled', () => { + expect( + transformDataSourceInResponse({ + configurationService: createConfigurationService({ + isSubscriptionEnabled: false + }), + dataSource: DataSource.YAHOO + }) + ).toBe(DataSource.YAHOO); + }); + }); +}); diff --git a/apps/api/src/helper/data-source.helper.ts b/apps/api/src/helper/data-source.helper.ts index fff8eaac1d..79802883e8 100644 --- a/apps/api/src/helper/data-source.helper.ts +++ b/apps/api/src/helper/data-source.helper.ts @@ -1,6 +1,13 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; + import { DataSource } from '@prisma/client'; import { createHash } from 'node:crypto'; +const DATA_SOURCES_WITHOUT_ENCODING: DataSource[] = [ + DataSource.GHOSTFOLIO, + DataSource.MANUAL +]; + const encodedDataSourceByDataSource = new Map( Object.values(DataSource).map((dataSource) => { return [dataSource, hashDataSource(dataSource)]; @@ -30,6 +37,16 @@ function deprecatedHashDataSource(dataSource: DataSource) { return Buffer.from(dataSource, 'utf-8').toString('hex'); } +function getGhostfolioDataSources({ + configurationService +}: { + configurationService: ConfigurationService; +}) { + return configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') + ? configurationService.get('DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER') + : []; +} + function hashDataSource(dataSource: DataSource) { return createHash('sha256').update(dataSource).digest('hex').slice(0, 8); } @@ -77,3 +94,51 @@ export function getUnmaskedGhostfolioDataSource({ ? (ghostfolioDataSources[0] as DataSource) : dataSource; } + +export function isDataSourceEncodedInResponse(dataSource: DataSource) { + return !DATA_SOURCES_WITHOUT_ENCODING.includes(dataSource); +} + +export function isValidEncodedDataSource(encodedDataSource: string) { + return dataSourceByEncodedDataSource.has(encodedDataSource); +} + +/** + * Gives the data source of a request without a transformation: an encoded data + * source is decoded, and the mask of the Ghostfolio data provider is resolved + * (see the TransformDataSourceInRequestInterceptor) + */ +export function transformDataSourceInRequest({ + configurationService, + dataSource +}: { + configurationService: ConfigurationService; + dataSource?: string; +}) { + if (Object.hasOwn(DataSource, dataSource)) { + return getUnmaskedGhostfolioDataSource({ + dataSource: dataSource as DataSource, + ghostfolioDataSources: getGhostfolioDataSources({ configurationService }) + }); + } + + return decodeDataSource(dataSource) as DataSource; +} + +/** + * Gives the data source as a response gives it to a user who is not an admin: + * encoded if the subscription is enabled, except the data sources GHOSTFOLIO + * and MANUAL (see the TransformDataSourceInResponseInterceptor) + */ +export function transformDataSourceInResponse({ + configurationService, + dataSource +}: { + configurationService: ConfigurationService; + dataSource: DataSource; +}) { + return configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + isDataSourceEncodedInResponse(dataSource) + ? encodeDataSource(dataSource) + : dataSource; +} diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 339b91a2f7..968159cbac 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -1,6 +1,7 @@ import { encodeDataSource, - getMaskedGhostfolioDataSource + getMaskedGhostfolioDataSource, + isDataSourceEncodedInResponse } from '@ghostfolio/api/helper/data-source.helper'; import { redactPaths } from '@ghostfolio/api/helper/object.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -30,7 +31,7 @@ export class TransformDataSourceInResponseInterceptor< if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { this.encodedDataSourceMap = Object.keys(DataSource).reduce( (encodedDataSourceMap, dataSource) => { - if (!['GHOSTFOLIO', 'MANUAL'].includes(dataSource)) { + if (isDataSourceEncodedInResponse(DataSource[dataSource])) { encodedDataSourceMap[dataSource] = encodeDataSource( DataSource[dataSource] ); diff --git a/apps/api/src/services/portfolio-table/interfaces/data-source-table-context.interface.ts b/apps/api/src/services/portfolio-table/interfaces/data-source-table-context.interface.ts new file mode 100644 index 0000000000..927f32cda0 --- /dev/null +++ b/apps/api/src/services/portfolio-table/interfaces/data-source-table-context.interface.ts @@ -0,0 +1,5 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; + +export interface DataSourceTableContext { + configurationService: ConfigurationService; +} diff --git a/apps/api/src/services/portfolio-table/interfaces/holdings-table-context.interface.ts b/apps/api/src/services/portfolio-table/interfaces/holdings-table-context.interface.ts index 4364b492e7..771cd10404 100644 --- a/apps/api/src/services/portfolio-table/interfaces/holdings-table-context.interface.ts +++ b/apps/api/src/services/portfolio-table/interfaces/holdings-table-context.interface.ts @@ -1,6 +1,8 @@ import { AssetClass, AssetSubClass } from '@prisma/client'; -export interface HoldingsTableContext { +import { DataSourceTableContext } from './data-source-table-context.interface'; + +export interface HoldingsTableContext extends DataSourceTableContext { assetClassTranslations: Record; assetSubClassTranslations: Record; } diff --git a/apps/api/src/services/portfolio-table/portfolio-table.module.ts b/apps/api/src/services/portfolio-table/portfolio-table.module.ts index 0995dc9980..01d0ebc64d 100644 --- a/apps/api/src/services/portfolio-table/portfolio-table.module.ts +++ b/apps/api/src/services/portfolio-table/portfolio-table.module.ts @@ -1,6 +1,7 @@ import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { WatchlistModule } from '@ghostfolio/api/app/endpoints/watchlist/watchlist.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; +import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module'; import { Module } from '@nestjs/common'; @@ -9,7 +10,13 @@ import { PortfolioTableService } from './portfolio-table.service'; @Module({ exports: [PortfolioTableService], - imports: [ActivitiesModule, I18nModule, PortfolioModule, WatchlistModule], + imports: [ + ActivitiesModule, + ConfigurationModule, + I18nModule, + PortfolioModule, + WatchlistModule + ], providers: [PortfolioTableService] }) export class PortfolioTableModule {} diff --git a/apps/api/src/services/portfolio-table/portfolio-table.service.spec.ts b/apps/api/src/services/portfolio-table/portfolio-table.service.spec.ts index ce92747c40..3cfb9fe856 100644 --- a/apps/api/src/services/portfolio-table/portfolio-table.service.spec.ts +++ b/apps/api/src/services/portfolio-table/portfolio-table.service.spec.ts @@ -1,12 +1,16 @@ +import type { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import type { WatchlistService } from '@ghostfolio/api/app/endpoints/watchlist/watchlist.service'; import type { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper'; import type { TableParameters } from '@ghostfolio/api/helper/interfaces/table-parameters.interface'; +import type { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import type { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { DEFAULT_LANGUAGE_CODE, TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { + Activity, HistoricalDataItem, PortfolioPerformanceResponse, PortfolioPosition, @@ -60,6 +64,8 @@ jest.mock('@ghostfolio/api/helper/markdown-table.helper', () => { }; }); +const SUBSCRIPTION_CONFIGURATION = { ENABLE_FEATURE_SUBSCRIPTION: true }; + function createAccount({ id = 'account-a-id', isExcluded = false, @@ -82,15 +88,39 @@ function createAccount({ } as unknown as AccountWithValue; } +function createActivity({ + dataSource = DataSource.YAHOO, + symbol = 'AAPL' +}: { + dataSource?: DataSource; + symbol?: string; +} = {}) { + return { + account: { name: 'Account A' }, + assetProfile: { + dataSource, + symbol, + currency: 'CHF', + name: `Name of ${symbol}` + }, + currency: 'CHF', + date: new Date('2024-01-01'), + type: 'BUY', + unitPrice: 100 + } as unknown as Activity; +} + function createHolding({ allocationInPercentage = 0.75, assetClass = AssetClass.EQUITY, assetSubClass = AssetSubClass.STOCK, + dataSource = DataSource.YAHOO, symbol = 'AAPL' }: { allocationInPercentage?: number; assetClass?: AssetClass; assetSubClass?: AssetSubClass; + dataSource?: DataSource; symbol?: string; } = {}) { return { @@ -99,6 +129,7 @@ function createHolding({ assetProfile: { assetClass, assetSubClass, + dataSource, symbol, currency: 'CHF', name: `Name of ${symbol}` @@ -132,18 +163,20 @@ function createPerformance({ } function createWatchlistItem({ + dataSource = DataSource.YAHOO, name = 'Name of AAPL', performancePercent = -0.25, symbol = 'AAPL' }: { + dataSource?: DataSource; name?: string; performancePercent?: number; symbol?: string; } = {}): WatchlistResponse['watchlist'][number] { return { + dataSource, name, symbol, - dataSource: DataSource.YAHOO, marketCondition: 'BEAR_MARKET', performances: { allTimeHigh: { @@ -158,17 +191,33 @@ function createWatchlistItem({ function createPortfolioTableService({ accounts = [], + activities = [], chart = [{ date: '2024-01-01' }], + configuration = { ENABLE_FEATURE_SUBSCRIPTION: false }, holdings = [], performance = createPerformance(), watchlist = [] }: { accounts?: AccountWithValue[]; + activities?: Activity[]; chart?: HistoricalDataItem[]; + configuration?: Record; holdings?: PortfolioPosition[]; performance?: PortfolioPerformanceResponse['performance']; watchlist?: WatchlistResponse['watchlist']; } = {}) { + const activitiesService = { + getActivities: jest + .fn() + .mockResolvedValue({ activities, count: activities.length }) + } as unknown as ActivitiesService; + + const configurationService = { + get: jest.fn((key: string) => { + return configuration[key]; + }) + } as unknown as ConfigurationService; + // The mock gives the identifier of the translation, so that a test can tell // the translation of the asset class from that of the asset sub class const i18nService = { @@ -188,7 +237,8 @@ function createPortfolioTableService({ } as unknown as WatchlistService; return new PortfolioTableService( - null, + activitiesService, + configurationService, i18nService, portfolioService, watchlistService @@ -220,6 +270,7 @@ describe('PortfolioTableService', () => { 'Type', 'Name', 'Symbol', + 'Data Source', 'Currency', 'Unit Price', 'Account' @@ -232,6 +283,7 @@ describe('PortfolioTableService', () => { expect(PortfolioTableService.getHoldingsTableColumnNames()).toEqual([ 'Name', 'Symbol', + 'Data Source', 'Currency', 'Asset Class', 'Asset Sub Class', @@ -257,6 +309,7 @@ describe('PortfolioTableService', () => { expect(PortfolioTableService.getWatchlistTableColumnNames()).toEqual([ 'Name', 'Symbol', + 'Data Source', 'Trend 50 Days', 'Trend 200 Days', 'Date of Last All Time High', @@ -327,14 +380,118 @@ describe('PortfolioTableService', () => { expect(result).toContain('No accounts found.'); }); }); + + describe('getActivitiesTable', () => { + it('gives the data source, encoded if the subscription is enabled', async () => { + const result = await createPortfolioTableService({ + activities: [ + createActivity({ dataSource: DataSource.YAHOO, symbol: 'AAPL' }), + createActivity({ + dataSource: DataSource.MANUAL, + symbol: 'GF_GOLD' + }) + ], + configuration: SUBSCRIPTION_CONFIGURATION + }).getActivitiesTable({ + take: 50, + userCurrency: 'CHF', + userId: 'user-id' + }); + + const [rowOfAapl, rowOfGold] = result.split('\n').filter((line) => { + return line.startsWith('| 2024-01-01'); + }); + + expect(rowOfAapl).toContain(`| ${encodeDataSource(DataSource.YAHOO)} |`); + expect(rowOfGold).toContain(`| ${DataSource.MANUAL} |`); + }); + }); + describe('getHoldingsTable', () => { - function getHoldingsTable(holdings: PortfolioPosition[]) { - return createPortfolioTableService({ holdings }).getHoldingsTable({ + function getHoldingsTable( + holdings: PortfolioPosition[], + { + configuration, + withDataSource + }: { + configuration?: Record; + withDataSource?: boolean; + } = {} + ) { + return createPortfolioTableService({ + configuration, + holdings + }).getHoldingsTable({ + withDataSource, languageCode: DEFAULT_LANGUAGE_CODE, userId: 'user-id' }); } + it('gives no data source by default', async () => { + const result = await getHoldingsTable([createHolding()], { + configuration: SUBSCRIPTION_CONFIGURATION + }); + + expect(result).not.toContain('Data Source'); + expect(result).not.toContain(encodeDataSource(DataSource.YAHOO)); + }); + + it('gives the data source, encoded if the subscription is enabled', async () => { + const result = await getHoldingsTable( + [ + createHolding({ dataSource: DataSource.YAHOO, symbol: 'AAPL' }), + createHolding({ + allocationInPercentage: 0.25, + dataSource: DataSource.MANUAL, + symbol: 'GF_GOLD' + }) + ], + { configuration: SUBSCRIPTION_CONFIGURATION, withDataSource: true } + ); + + const [rowOfAapl, rowOfGold] = result.split('\n').filter((line) => { + return line.startsWith('| Name of'); + }); + + expect(rowOfAapl).toContain(`| ${encodeDataSource(DataSource.YAHOO)} |`); + expect(rowOfGold).toContain(`| ${DataSource.MANUAL} |`); + }); + + it('gives the data source unencoded if the subscription is disabled', async () => { + const result = await getHoldingsTable([createHolding()], { + configuration: { ENABLE_FEATURE_SUBSCRIPTION: false }, + withDataSource: true + }); + + const [row] = result.split('\n').filter((line) => { + return line.startsWith('| Name of AAPL'); + }); + + expect(row).toContain(`| ${DataSource.YAHOO} |`); + }); + + it('gives no data source of a cash position', async () => { + const result = await getHoldingsTable( + [ + createHolding({ + assetClass: AssetClass.LIQUIDITY, + assetSubClass: AssetSubClass.CASH, + dataSource: DataSource.YAHOO, + symbol: 'USD' + }) + ], + { configuration: SUBSCRIPTION_CONFIGURATION, withDataSource: true } + ); + + const [row] = result.split('\n').filter((line) => { + return line.startsWith('| Name of USD'); + }); + + expect(row).toContain('| USD | | CHF |'); + expect(row).not.toContain(encodeDataSource(DataSource.YAHOO)); + }); + it('gives the translation of the asset class and of the asset sub class', async () => { const result = await getHoldingsTable([createHolding()]); @@ -417,12 +574,39 @@ describe('PortfolioTableService', () => { }); describe('getWatchlistTable', () => { - function getWatchlistTable(watchlist: WatchlistResponse['watchlist']) { - return createPortfolioTableService({ watchlist }).getWatchlistTable({ + function getWatchlistTable( + watchlist: WatchlistResponse['watchlist'], + configuration?: Record + ) { + return createPortfolioTableService({ + configuration, + watchlist + }).getWatchlistTable({ userId: 'user-id' }); } + it('gives the data source, encoded if the subscription is enabled', async () => { + const result = await getWatchlistTable( + [ + createWatchlistItem(), + createWatchlistItem({ + dataSource: DataSource.MANUAL, + name: 'Name of GF_GOLD', + symbol: 'GF_GOLD' + }) + ], + SUBSCRIPTION_CONFIGURATION + ); + + const [rowOfAapl, rowOfGold] = result.split('\n').filter((line) => { + return line.startsWith('| Name of'); + }); + + expect(rowOfAapl).toContain(`| ${encodeDataSource(DataSource.YAHOO)} |`); + expect(rowOfGold).toContain(`| ${DataSource.MANUAL} |`); + }); + it('gives the date and the change of the all time high', async () => { const result = await getWatchlistTable([createWatchlistItem()]); diff --git a/apps/api/src/services/portfolio-table/portfolio-table.service.ts b/apps/api/src/services/portfolio-table/portfolio-table.service.ts index a9c4c36121..9e63d133b5 100644 --- a/apps/api/src/services/portfolio-table/portfolio-table.service.ts +++ b/apps/api/src/services/portfolio-table/portfolio-table.service.ts @@ -1,10 +1,16 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { WatchlistService } from '@ghostfolio/api/app/endpoints/watchlist/watchlist.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { transformDataSourceInResponse } from '@ghostfolio/api/helper/data-source.helper'; import { TableColumnDefinition } from '@ghostfolio/api/helper/interfaces/table-column-definition.interface'; import { getMarkdownTable } from '@ghostfolio/api/helper/markdown-table.helper'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; -import { DATE_FORMAT, isAccountExcluded } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + isAccountExcluded, + isCashPosition +} from '@ghostfolio/common/helper'; import { Activity, Filter, @@ -17,12 +23,31 @@ import { Injectable } from '@nestjs/common'; import { AssetClass, AssetSubClass, + DataSource, Type as ActivityType } from '@prisma/client'; import { format } from 'date-fns'; +import { DataSourceTableContext } from './interfaces/data-source-table-context.interface'; import { HoldingsTableColumnDefinition } from './types/holdings-table-column-definition.type'; +const DATA_SOURCE_COLUMN_NAME = 'Data Source'; + +function getDataSourceColumnDefinition( + getDataSource: (row: T) => DataSource | undefined +): TableColumnDefinition { + return { + getValue: (row, { configurationService }) => { + const dataSource = getDataSource(row); + + return dataSource + ? transformDataSourceInResponse({ configurationService, dataSource }) + : ''; + }, + name: DATA_SOURCE_COLUMN_NAME + }; +} + function getPercentage(value: number) { return `${(value * 100).toFixed(3)}%`; } @@ -83,52 +108,57 @@ export class PortfolioTableService { } ]; - private static readonly ACTIVITIES_TABLE_COLUMN_DEFINITIONS: TableColumnDefinition[] = - [ - { - getValue: ({ date }) => { - return format(date, DATE_FORMAT); - }, - name: 'Date' + private static readonly ACTIVITIES_TABLE_COLUMN_DEFINITIONS: TableColumnDefinition< + Activity, + DataSourceTableContext + >[] = [ + { + getValue: ({ date }) => { + return format(date, DATE_FORMAT); }, - { - getValue: ({ type }) => { - return type; - }, - name: 'Type' + name: 'Date' + }, + { + getValue: ({ type }) => { + return type; }, - { - getValue: ({ assetProfile }) => { - return assetProfile.name ?? ''; - }, - name: 'Name' + name: 'Type' + }, + { + getValue: ({ assetProfile }) => { + return assetProfile.name ?? ''; }, - { - getValue: ({ assetProfile }) => { - return assetProfile.symbol; - }, - name: 'Symbol' + name: 'Name' + }, + { + getValue: ({ assetProfile }) => { + return assetProfile.symbol; }, - { - getValue: ({ assetProfile, currency }) => { - return currency ?? assetProfile.currency; - }, - name: 'Currency' + name: 'Symbol' + }, + getDataSourceColumnDefinition(({ assetProfile }) => { + return assetProfile.dataSource; + }), + { + getValue: ({ assetProfile, currency }) => { + return currency ?? assetProfile.currency; }, - { - align: 'right', - getValue: ({ unitPrice }) => { - return unitPrice.toString(); - }, - name: 'Unit Price' + name: 'Currency' + }, + { + align: 'right', + getValue: ({ unitPrice }) => { + return unitPrice.toString(); }, - { - getValue: ({ account }) => { - return account?.name ?? ''; - }, - name: 'Account' - } - ]; + name: 'Unit Price' + }, + { + getValue: ({ account }) => { + return account?.name ?? ''; + }, + name: 'Account' + } + ]; private static readonly HOLDINGS_TABLE_COLUMN_DEFINITIONS: HoldingsTableColumnDefinition[] = [ @@ -144,6 +174,11 @@ export class PortfolioTableService { }, name: 'Symbol' }, + getDataSourceColumnDefinition(({ assetProfile }) => { + return isCashPosition(assetProfile) + ? undefined + : assetProfile.dataSource; + }), { getValue: ({ assetProfile }) => { return assetProfile.currency; @@ -222,7 +257,8 @@ export class PortfolioTableService { ]; private static readonly WATCHLIST_TABLE_COLUMN_DEFINITIONS: TableColumnDefinition< - WatchlistResponse['watchlist'][number] + WatchlistResponse['watchlist'][number], + DataSourceTableContext >[] = [ { getValue: ({ name }) => { @@ -236,6 +272,9 @@ export class PortfolioTableService { }, name: 'Symbol' }, + getDataSourceColumnDefinition(({ dataSource }) => { + return dataSource; + }), { getValue: ({ trend50d }) => { return trend50d; @@ -273,6 +312,7 @@ export class PortfolioTableService { public constructor( private readonly activitiesService: ActivitiesService, + private readonly configurationService: ConfigurationService, private readonly i18nService: I18nService, private readonly portfolioService: PortfolioService, private readonly watchlistService: WatchlistService @@ -399,6 +439,7 @@ export class PortfolioTableService { await getMarkdownTable({ columnDefinitions: PortfolioTableService.ACTIVITIES_TABLE_COLUMN_DEFINITIONS, + context: { configurationService: this.configurationService }, rows: activities }) ); @@ -410,11 +451,13 @@ export class PortfolioTableService { public async getHoldingsTable({ filters, languageCode, - userId + userId, + withDataSource = false }: { filters?: Filter[]; languageCode: string; userId: string; + withDataSource?: boolean; }) { const { holdings } = await this.portfolioService.getDetails({ filters, @@ -442,8 +485,16 @@ export class PortfolioTableService { '', await getMarkdownTable({ columnDefinitions: - PortfolioTableService.HOLDINGS_TABLE_COLUMN_DEFINITIONS, - context: { assetClassTranslations, assetSubClassTranslations }, + PortfolioTableService.HOLDINGS_TABLE_COLUMN_DEFINITIONS.filter( + ({ name }) => { + return withDataSource || name !== DATA_SOURCE_COLUMN_NAME; + } + ), + context: { + assetClassTranslations, + assetSubClassTranslations, + configurationService: this.configurationService + }, rows: sortedHoldings }) ].join('\n'); @@ -491,6 +542,7 @@ export class PortfolioTableService { await getMarkdownTable({ columnDefinitions: PortfolioTableService.WATCHLIST_TABLE_COLUMN_DEFINITIONS, + context: { configurationService: this.configurationService }, rows: watchlist }) ); From ce68375f6fefbf28a0222341ce64e256907131f0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 11:01:23 +0200 Subject: [PATCH 20/35] Bugfix/positive performance from all time high in watchlist (#8012) * Fix positive performance from all time high * Update changelog --- CHANGELOG.md | 1 + .../app/endpoints/watchlist/watchlist.service.ts | 7 +++++-- .../services/benchmark/benchmark.service.spec.ts | 10 ++++++++++ .../api/src/services/benchmark/benchmark.service.ts | 13 +++++++++---- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d64f3d12f3..7149a52583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed the positive performance from the all time high in the watchlist - Fixed the portfolio calculation for holdings with historical market prices between the chart dates ## 3.77.0 - 2026-10-02 diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts index c69a31df82..925f49cd6b 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts @@ -140,8 +140,11 @@ export class WatchlistService { name: assetProfile?.name, performances: { allTimeHigh: { - performancePercent, - date: allTimeHigh?.date + date: allTimeHigh?.date, + performancePercent: + this.benchmarkService.getPerformancePercentFromAllTimeHigh( + performancePercent + ) } }, trend50d: trends.trend50d, diff --git a/apps/api/src/services/benchmark/benchmark.service.spec.ts b/apps/api/src/services/benchmark/benchmark.service.spec.ts index 0e7119b045..27124e89e4 100644 --- a/apps/api/src/services/benchmark/benchmark.service.spec.ts +++ b/apps/api/src/services/benchmark/benchmark.service.spec.ts @@ -24,4 +24,14 @@ describe('BenchmarkService', () => { ); expect(benchmarkService.getMarketCondition(-0.2)).toEqual('BEAR_MARKET'); }); + + it('getPerformancePercentFromAllTimeHigh', async () => { + expect(benchmarkService.getPerformancePercentFromAllTimeHigh(0.1)).toEqual( + 0 + ); + expect(benchmarkService.getPerformancePercentFromAllTimeHigh(0)).toEqual(0); + expect(benchmarkService.getPerformancePercentFromAllTimeHigh(-0.1)).toEqual( + -0.1 + ); + }); }); diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts index 4ea27b2f2e..3c39848401 100644 --- a/apps/api/src/services/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -216,6 +216,12 @@ export class BenchmarkService { } } + public getPerformancePercentFromAllTimeHigh(performancePercent: number) { + const isAtAllTimeHigh = performancePercent >= 0; + + return isAtAllTimeHigh ? 0 : performancePercent; + } + public async isBenchmark(symbolProfileId: string): Promise { const benchmarks = await this.getBenchmarksProperty(); @@ -288,10 +294,9 @@ export class BenchmarkService { performances: { allTimeHigh: { date: allTimeHigh?.date, - performancePercent: - performancePercentFromAllTimeHigh >= 0 - ? 0 - : performancePercentFromAllTimeHigh + performancePercent: this.getPerformancePercentFromAllTimeHigh( + performancePercentFromAllTimeHigh + ) } }, symbol: benchmarkAssetProfiles[index].symbol, From 5dc53487d38c551ba5a80fea6788485db16add77 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 13:35:54 +0200 Subject: [PATCH 21/35] Bugfix/data gathering of manual activities (#7997) * Fix data gathering of manual activities * Update changelog --- CHANGELOG.md | 1 + .../app/activities/activities.controller.ts | 18 +++++++++------ .../src/app/activities/activities.service.ts | 2 +- apps/api/src/helper/data-source.helper.ts | 15 ++++++++++++- .../data-gathering/data-gathering.service.ts | 22 ++++--------------- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7149a52583..73f5227a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the positive performance from the all time high in the watchlist - Fixed the portfolio calculation for holdings with historical market prices between the chart dates +- Fixed the asset profile identifier in the historical market data gathering of the `POST api/v1/activities` endpoint ## 3.77.0 - 2026-10-02 diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 0ed1d781c1..90851b2ca4 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorator'; import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator'; import { isActivityInFuture } from '@ghostfolio/api/helper/activity.helper'; +import { isDataGatheringSupported } from '@ghostfolio/api/helper/data-source.helper'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; @@ -32,7 +33,7 @@ import { Query, UseInterceptors } from '@nestjs/common'; -import { Order } from '@prisma/client'; +import { Order, Prisma } from '@prisma/client'; import { parseISO } from 'date-fns'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -216,7 +217,7 @@ export class ActivitiesController { userId, userSubscription }: ImpersonationContext - ): Promise { + ): Promise> { // Evaluate the more restrictive subscription of the authenticated user // and the owner of the activity const subscription = @@ -284,15 +285,18 @@ export class ActivitiesController { user: { connect: { id: userId } } }); - if (dataSource && !isActivityInFuture({ date: activity.date })) { - // Gather symbol data in the background, if data source is set - // (not MANUAL) and the date is not in the future + if ( + isDataGatheringSupported(activity.SymbolProfile) && + !isActivityInFuture({ date: activity.date }) + ) { + // Gather symbol data in the background, if the asset profile supports + // data gathering and the date is not in the future this.dataGatheringService.gatherSymbols({ dataGatheringItems: [ { - dataSource, + dataSource: activity.SymbolProfile.dataSource, date: activity.date, - symbol: data.symbol + symbol: activity.SymbolProfile.symbol } ], priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 59d1ba9c33..75adcbc5d3 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -183,7 +183,7 @@ export class ActivitiesService { updateAccountBalance?: boolean; userId: string; } - ): Promise { + ): Promise> { const tags = data.tags ?? []; await this.tagService.validateTagIds({ diff --git a/apps/api/src/helper/data-source.helper.ts b/apps/api/src/helper/data-source.helper.ts index 79802883e8..19a26816d9 100644 --- a/apps/api/src/helper/data-source.helper.ts +++ b/apps/api/src/helper/data-source.helper.ts @@ -1,6 +1,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { DataSource } from '@prisma/client'; +import { DataSource, Prisma } from '@prisma/client'; +import { isEmpty } from 'lodash-es'; import { createHash } from 'node:crypto'; const DATA_SOURCES_WITHOUT_ENCODING: DataSource[] = [ @@ -95,6 +96,18 @@ export function getUnmaskedGhostfolioDataSource({ : dataSource; } +export function isDataGatheringSupported({ + dataSource, + scraperConfiguration +}: { + dataSource: DataSource; + scraperConfiguration: Prisma.JsonValue; +}) { + // An asset profile with the MANUAL data source supports data gathering only + // with a scraper configuration + return dataSource !== DataSource.MANUAL || !isEmpty(scraperConfiguration); +} + export function isDataSourceEncodedInResponse(dataSource: DataSource) { return !DATA_SOURCES_WITHOUT_ENCODING.includes(dataSource); } diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts index 3a98757591..1162143421 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.service.ts @@ -1,3 +1,4 @@ +import { isDataGatheringSupported } from '@ghostfolio/api/helper/data-source.helper'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; @@ -33,7 +34,6 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { Job, JobOptions, Queue } from 'bull'; import { format, min, subDays, subMilliseconds, subYears } from 'date-fns'; -import { isEmpty } from 'lodash-es'; import ms, { StringValue } from 'ms'; @Injectable() @@ -506,12 +506,7 @@ export class DataGatheringService { return symbolProfiles .filter(({ dataSource, scraperConfiguration }) => { - const manualDataSourceWithScraperConfiguration = - dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); - - return ( - dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration - ); + return isDataGatheringSupported({ dataSource, scraperConfiguration }); }) .map(({ dataSource, symbol }) => { return { dataSource, symbol }; @@ -534,14 +529,10 @@ export class DataGatheringService { return symbolProfiles .filter(({ dataSource, scraperConfiguration, symbol }) => { - const manualDataSourceWithScraperConfiguration = - dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); - return ( !assetProfileIdentifiersWithRecentMarketData.some((item) => { return item.dataSource === dataSource && item.symbol === symbol; - }) && - (dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration) + }) && isDataGatheringSupported({ dataSource, scraperConfiguration }) ); }) .map((symbolProfile) => { @@ -598,12 +589,7 @@ export class DataGatheringService { }) ) .filter(({ dataSource, scraperConfiguration }) => { - const manualDataSourceWithScraperConfiguration = - dataSource === 'MANUAL' && !isEmpty(scraperConfiguration); - - return ( - dataSource !== 'MANUAL' || manualDataSourceWithScraperConfiguration - ); + return isDataGatheringSupported({ dataSource, scraperConfiguration }); }) .map((symbolProfile) => { let date = symbolProfile.activities?.[0]?.date ?? startDate; From c0aea2ac4aa924c4be8c12925c3a556b319a0aa0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:54:01 +0200 Subject: [PATCH 22/35] Task/exclude cash position in base currency from holdings in overview (#8022) * Exclude cash position in base currency * Update changelog --- CHANGELOG.md | 1 + .../home-overview/home-overview.component.ts | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73f5227a41..6207c1c202 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 label of the cash positions in the holdings charts and table +- Excluded the cash position in the base currency from the holdings table on the overview tab of the home page (experimental) - Extended the tools to get the activities, the portfolio and the watchlist in the server of the Model Context Protocol (MCP) to include the data source (experimental) - Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` - Upgraded `ai` from version `7.0.37` to `7.0.114` diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index 7715ef5719..b677f88a39 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -8,6 +8,7 @@ import { DEFAULT_LOCALE, NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; +import { isCashPosition } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, LineChartItem, @@ -153,6 +154,8 @@ export class GfHomeOverviewComponent implements OnInit { } private update() { + const userSettings = this.user()?.settings; + this.historicalDataItems.set(null); this.isLoadingPerformance.set(true); @@ -162,17 +165,27 @@ export class GfHomeOverviewComponent implements OnInit { this.dataService .fetchPortfolioHoldings({ filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }], - range: this.user()?.settings?.dateRange + range: userSettings?.dateRange }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { - this.holdings.set(holdings); + const baseCurrency = userSettings?.baseCurrency ?? DEFAULT_CURRENCY; + + this.holdings.set( + holdings.filter(({ assetProfile }) => { + return !( + isCashPosition(assetProfile) && + assetProfile.currency === baseCurrency && + assetProfile.symbol === baseCurrency + ); + }) + ); }); } this.dataService .fetchPortfolioPerformance({ - range: this.user()?.settings?.dateRange ?? DEFAULT_DATE_RANGE + range: userSettings?.dateRange ?? DEFAULT_DATE_RANGE }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ chart, errors, performance }) => { From 9c1e014190fbb3dfaa368c4ba9bc841b8dd47933 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:03:33 +0200 Subject: [PATCH 23/35] Task/select required symbol profile fields in activities (#7998) * Select required symbol profile fields * Remove symbol profile relation from activities * Update changelog --- CHANGELOG.md | 1 + .../src/app/activities/activities.service.ts | 12 ++- apps/api/src/app/import/import.service.ts | 2 +- apps/api/src/helper/object.helper.spec.ts | 85 ++++++++++--------- ...orm-data-source-in-response.interceptor.ts | 2 - libs/common/src/lib/config.ts | 3 - 6 files changed, 57 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6207c1c202..43cd5a98eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the label of the cash positions in the holdings charts and table - Excluded the cash position in the base currency from the holdings table on the overview tab of the home page (experimental) - Extended the tools to get the activities, the portfolio and the watchlist in the server of the Model Context Protocol (MCP) to include the data source (experimental) +- Removed the deprecated `SymbolProfile` field from the endpoints `GET api/v1/activities`, `GET api/v1/activities/:id` and `POST api/v1/import` - Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` - Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 75adcbc5d3..9e1a1b8d4c 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -60,7 +60,7 @@ import { Type as ActivityType } from '@prisma/client'; import { Big } from 'big.js'; -import { groupBy, uniqBy } from 'lodash-es'; +import { groupBy, omit, uniqBy } from 'lodash-es'; import { randomUUID } from 'node:crypto'; @Injectable() @@ -655,7 +655,13 @@ export class ActivitiesService { } }, // eslint-disable-next-line @typescript-eslint/naming-convention - SymbolProfile: true, + SymbolProfile: { + select: { + currency: true, + dataSource: true, + symbol: true + } + }, tags: true }, orderBy: [...orderBy, { id: sortDirection }] @@ -736,7 +742,7 @@ export class ActivitiesService { ]); return { - ...order, + ...omit(order, ['SymbolProfile']), assetProfile, feeInAssetProfileCurrency, feeInBaseCurrency, diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 5afa4c33dd..705066441f 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -1004,7 +1004,7 @@ export class ImportService { )) ?? 0; activities.push({ - ...order, + ...omit(order, ['SymbolProfile']), // @ts-ignore assetProfile, error, diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index 9776232a0b..5543441dbc 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -3082,20 +3082,6 @@ describe('redactAttributes', () => { feeInBaseCurrency: 18.2, id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', quantity: 50, - // The relation of the database is part of the response next to the - // asset profile - SymbolProfile: { - comment: 'Reviewed by the administrator', - name: 'Apple Inc', - scraperConfiguration: { - headers: { Authorization: 'Bearer TOKEN' }, - mode: 'lazy', - selector: '.price', - url: 'https://example.org/get_price' - }, - symbol: 'AAPL', - symbolMapping: { YAHOO: 'AAPL' } - }, type: 'BUY', unitPrice: 220.79, value: 11039.5, @@ -3125,6 +3111,49 @@ describe('redactAttributes', () => { feeInBaseCurrency: null, id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', quantity: null, + type: 'BUY', + // A price per unit stays visible, like the average price and the market + // price of a holding + unitPrice: 220.79, + value: null, + valueInBaseCurrency: null + }); + }); + + // The endpoint POST api/v1/activities returns a row of the database with the + // relation to the symbol profile + it('should redact an activity with the relation to the symbol profile', () => { + expect( + redactPaths({ + object: { + comment: 'Bought on a dip', + currency: 'USD', + fee: 19.9, + id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', + quantity: 50, + SymbolProfile: { + comment: 'Reviewed by the administrator', + name: 'Apple Inc', + scraperConfiguration: { + headers: { Authorization: 'Bearer TOKEN' }, + mode: 'lazy', + selector: '.price', + url: 'https://example.org/get_price' + }, + symbol: 'AAPL', + symbolMapping: { YAHOO: 'AAPL' } + }, + type: 'BUY', + unitPrice: 220.79 + }, + paths: DEFAULT_REDACTED_PATHS + }) + ).toStrictEqual({ + comment: null, + currency: 'USD', + fee: null, + id: '8c623328-6035-4b5f-b6d5-702cc1c9c56b', + quantity: null, SymbolProfile: { comment: null, name: 'Apple Inc', @@ -3133,15 +3162,12 @@ describe('redactAttributes', () => { symbolMapping: null }, type: 'BUY', - // A price per unit stays visible, like the average price and the market - // price of a holding - unitPrice: 220.79, - value: null, - valueInBaseCurrency: null + unitPrice: 220.79 }); }); - // The write endpoints return a row of the database, which has no relation + // The endpoints PUT and DELETE api/v1/activities/:id return a row of the + // database without the relations it('should redact an activity without the relations', () => { expect( redactPaths({ @@ -3190,18 +3216,6 @@ describe('redactAttributes', () => { currency: 'USD', fee: 19.9, quantity: 50, - SymbolProfile: { - comment: 'Reviewed by the administrator', - name: 'Apple Inc', - scraperConfiguration: { - headers: { Authorization: 'Bearer TOKEN' }, - mode: 'lazy', - selector: '.price', - url: 'https://example.org/get_price' - }, - symbol: 'AAPL', - symbolMapping: { YAHOO: 'AAPL' } - }, type: 'BUY', unitPrice: 220.79, value: 11039.5, @@ -3227,13 +3241,6 @@ describe('redactAttributes', () => { currency: 'USD', fee: null, quantity: null, - SymbolProfile: { - comment: null, - name: 'Apple Inc', - scraperConfiguration: null, - symbol: 'AAPL', - symbolMapping: null - }, type: 'BUY', unitPrice: 220.79, value: null, diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 968159cbac..eedc808772 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -84,7 +84,6 @@ export class TransformDataSourceInResponseInterceptor< '["filters.dataSource"]', 'activities[*].assetProfile.dataSource', 'activities[*].dataSource', - 'activities[*].SymbolProfile.dataSource', 'assetProfile.dataSource', 'benchmarks[*].dataSource', 'errors[*].dataSource', @@ -95,7 +94,6 @@ export class TransformDataSourceInResponseInterceptor< 'items[*].dataSource', 'latestActivities[*].assetProfile.dataSource', 'settings["filters.dataSource"]', - 'SymbolProfile.dataSource', 'watchlist[*].dataSource' ] }); diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 5a8aecff36..dbf873df2a 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -138,9 +138,6 @@ export const DEFAULT_REDACTED_PATHS = [ 'activities[*].feeInAssetProfileCurrency', 'activities[*].feeInBaseCurrency', 'activities[*].quantity', - 'activities[*].SymbolProfile.comment', - 'activities[*].SymbolProfile.scraperConfiguration', - 'activities[*].SymbolProfile.symbolMapping', 'activities[*].value', 'activities[*].valueInBaseCurrency', 'assetProfile.comment', From c65e0856bbccd5a2322cdf984edd2ec6810007be Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:45:37 +0700 Subject: [PATCH 24/35] Task/widen input types in fire calculator component (#8015) Widen input types --- .../fire-calculator.component.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index bdd65d1668..64bcec2934 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -2,7 +2,7 @@ import { getTooltipOptions, transformTickToAbbreviation } from '@ghostfolio/common/chart-helper'; -import { primaryColorRgb } from '@ghostfolio/common/config'; +import { DEFAULT_CURRENCY, primaryColorRgb } from '@ghostfolio/common/config'; import { formatMonthAndYear, getLocale } from '@ghostfolio/common/helper'; import { FireCalculationCompleteEvent } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; @@ -53,6 +53,7 @@ import { addDays, addYears, getMonth, + parseISO, setMonth, setYear, startOfMonth, @@ -60,7 +61,7 @@ import { } from 'date-fns'; import { addIcons } from 'ionicons'; import { calendarClearOutline } from 'ionicons/icons'; -import { isNumber } from 'lodash-es'; +import { isNumber, isString } from 'lodash-es'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { debounceTime } from 'rxjs'; @@ -86,16 +87,16 @@ import { FireCalculatorService } from './fire-calculator.service'; templateUrl: './fire-calculator.component.html' }) export class GfFireCalculatorComponent implements OnChanges, OnDestroy { - @Input() annualInterestRate = 0; - @Input() colorScheme?: ColorScheme; - @Input() currency: string; - @Input() deviceType: string; - @Input() fireWealth = 0; - @Input() hasPermissionToUpdateUserSettings: boolean; - @Input() locale = getLocale(); - @Input() projectedTotalAmount = 0; - @Input() retirementDate: Date; - @Input() savingsRate = 0; + @Input() public annualInterestRate?: number = 0; + @Input() public colorScheme?: ColorScheme; + @Input() public currency?: string; + @Input() public deviceType: string; + @Input() public fireWealth = 0; + @Input() public hasPermissionToUpdateUserSettings: boolean; + @Input() public locale?: string = getLocale(); + @Input() public projectedTotalAmount?: number = 0; + @Input() public retirementDate?: Date | string; + @Input() public savingsRate?: number = 0; public calculatorForm = this.formBuilder.group({ annualInterestRate: new FormControl(null), @@ -243,11 +244,13 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { if (isNumber(this.fireWealth) && this.fireWealth >= 0) { this.calculatorForm.setValue( { - annualInterestRate: this.annualInterestRate, - paymentPerPeriod: this.savingsRate, + annualInterestRate: this.annualInterestRate ?? 0, + paymentPerPeriod: this.savingsRate ?? 0, principalInvestmentAmount: this.fireWealth, - projectedTotalAmount: this.projectedTotalAmount, - retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE + projectedTotalAmount: this.projectedTotalAmount ?? 0, + retirementDate: isString(this.retirementDate) + ? parseISO(this.retirementDate) + : (this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE) }, { emitEvent: false @@ -353,7 +356,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { ); return `Total: ${new Intl.NumberFormat(this.locale, { - currency: this.currency, + currency: this.currency ?? DEFAULT_CURRENCY, currencyDisplay: 'code', style: 'currency' }).format(totalAmount)}`; @@ -367,7 +370,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { if (context.parsed.y !== null) { label += new Intl.NumberFormat(this.locale, { - currency: this.currency, + currency: this.currency ?? DEFAULT_CURRENCY, currencyDisplay: 'code', style: 'currency' }).format(context.parsed.y); @@ -510,7 +513,8 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { } else { const today = new Date(); const retirementDate = - this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE; + this.calculatorForm.get('retirementDate')?.value ?? + this.DEFAULT_RETIREMENT_DATE; return ( 12 * (retirementDate.getFullYear() - today.getFullYear()) + From 93780862063c1e1a7e4d10d2ca171099cbf885c8 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:49:08 +0700 Subject: [PATCH 25/35] Task/widen input types in activities table component (#8016) Widen input types --- .../activities-table.component.html | 10 +++--- .../activities-table.component.ts | 36 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index ded8fe8c83..a093eef399 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -69,7 +69,7 @@ @if (canExportActivities()) { Export {{ totalItems > 1 ? totalItems : '' }} + >Export {{ (totalItems ?? 0) > 1 ? totalItems : '' }} {totalItems, plural, =1 {Activity} other {Activities}} } @else { @@ -107,7 +107,7 @@ @if (canDeleteActivities()) { Delete {{ totalItems > 1 ? totalItems : '' }} + >Delete {{ (totalItems ?? 0) > 1 ? totalItems : '' }} {totalItems, plural, =1 {Activity} other {Activities}} } @else { @@ -126,7 +126,7 @@ class="gf-table w-100" mat-table matSort - [dataSource]="dataSource()" + [dataSource]="dataSource() ?? []" [matSortActive]="sortColumn" [matSortDirection]="sortDirection" [matSortDisabled]="sortDisabled" @@ -574,9 +574,9 @@ } (); @Output() activityClicked = new EventEmitter(); From 933df54b98d18a11fc62a8dffb8ad6dbe2ade2fb Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:50:22 +0700 Subject: [PATCH 26/35] Task/widen input types in world map and top holdings components (#8017) Widen input types --- libs/ui/src/lib/top-holdings/top-holdings.component.ts | 10 +++++----- .../lib/world-map-chart/world-map-chart.component.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts index 1a675a5531..80caca9185 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts @@ -38,11 +38,11 @@ import { GfValueComponent } from '../value/value.component'; templateUrl: './top-holdings.component.html' }) export class GfTopHoldingsComponent implements OnChanges { - @Input() baseCurrency: string; - @Input() hasPermissionToShowValues = true; - @Input() locale = getLocale(); - @Input() pageSize = Number.MAX_SAFE_INTEGER; - @Input() topHoldings: HoldingWithParents[]; + @Input() public baseCurrency?: string; + @Input() public hasPermissionToShowValues = true; + @Input() public locale?: string = getLocale(); + @Input() public pageSize = Number.MAX_SAFE_INTEGER; + @Input() public topHoldings?: HoldingWithParents[]; @Output() holdingClicked = new EventEmitter(); diff --git a/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts b/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts index 899441b7be..29c66267a1 100644 --- a/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts +++ b/libs/ui/src/lib/world-map-chart/world-map-chart.component.ts @@ -23,10 +23,12 @@ import svgMap from 'svgmap'; templateUrl: './world-map-chart.component.html' }) export class GfWorldMapChartComponent implements OnChanges, OnDestroy { - @Input() countries: { [code: string]: { name?: string; value: number } }; - @Input() format: string; - @Input() isInPercentage = false; - @Input() locale = getLocale(); + @Input() public countries: { + [code: string]: { name?: string; value: number }; + }; + @Input() public format: string; + @Input() public isInPercentage?: boolean = false; + @Input() public locale?: string = getLocale(); public isLoading = true; public svgMapElement: any; From 8e86a4d2b1c4ac538ffe0de501ee85e088b7b270 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:12:31 +0200 Subject: [PATCH 27/35] Task/refactor transform data source in request interceptor (#8020) Refactoring --- .../api/src/helper/data-source.helper.spec.ts | 22 ++++++++ apps/api/src/helper/data-source.helper.ts | 28 +++++------ ...form-data-source-in-request.interceptor.ts | 50 +++++-------------- 3 files changed, 49 insertions(+), 51 deletions(-) diff --git a/apps/api/src/helper/data-source.helper.spec.ts b/apps/api/src/helper/data-source.helper.spec.ts index 02d979441e..3bfef82add 100644 --- a/apps/api/src/helper/data-source.helper.spec.ts +++ b/apps/api/src/helper/data-source.helper.spec.ts @@ -83,6 +83,28 @@ describe('DataSourceHelper', () => { }) ).toBeUndefined(); }); + + it('Decodes an encoded data source if the subscription is not enabled', () => { + expect( + transformDataSourceInRequest({ + configurationService: createConfigurationService({ + isSubscriptionEnabled: false + }), + dataSource: encodeDataSource(DataSource.COINGECKO) + }) + ).toBe(DataSource.COINGECKO); + }); + + it('Keeps the data source GHOSTFOLIO if the subscription is not enabled', () => { + expect( + transformDataSourceInRequest({ + configurationService: createConfigurationService({ + isSubscriptionEnabled: false + }), + dataSource: DataSource.GHOSTFOLIO + }) + ).toBe(DataSource.GHOSTFOLIO); + }); }); describe('transformDataSourceInResponse', () => { diff --git a/apps/api/src/helper/data-source.helper.ts b/apps/api/src/helper/data-source.helper.ts index 19a26816d9..fb0ded3f1f 100644 --- a/apps/api/src/helper/data-source.helper.ts +++ b/apps/api/src/helper/data-source.helper.ts @@ -48,6 +48,18 @@ function getGhostfolioDataSources({ : []; } +function getUnmaskedGhostfolioDataSource({ + dataSource, + ghostfolioDataSources +}: { + dataSource?: DataSource; + ghostfolioDataSources: string[]; +}) { + return dataSource === DataSource.GHOSTFOLIO && ghostfolioDataSources?.[0] + ? (ghostfolioDataSources[0] as DataSource) + : dataSource; +} + function hashDataSource(dataSource: DataSource) { return createHash('sha256').update(dataSource).digest('hex').slice(0, 8); } @@ -84,18 +96,6 @@ export function getMaskedGhostfolioDataSource({ : dataSource; } -export function getUnmaskedGhostfolioDataSource({ - dataSource, - ghostfolioDataSources -}: { - dataSource?: DataSource; - ghostfolioDataSources: string[]; -}) { - return dataSource === DataSource.GHOSTFOLIO && ghostfolioDataSources?.[0] - ? (ghostfolioDataSources[0] as DataSource) - : dataSource; -} - export function isDataGatheringSupported({ dataSource, scraperConfiguration @@ -118,8 +118,8 @@ export function isValidEncodedDataSource(encodedDataSource: string) { /** * Gives the data source of a request without a transformation: an encoded data - * source is decoded, and the mask of the Ghostfolio data provider is resolved - * (see the TransformDataSourceInRequestInterceptor) + * source is decoded, and the mask of the Ghostfolio data provider is resolved if + * the subscription is enabled (see the TransformDataSourceInRequestInterceptor) */ export function transformDataSourceInRequest({ configurationService, diff --git a/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts index 7b537cd523..569ffc60ef 100644 --- a/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor.ts @@ -1,6 +1,6 @@ import { decodeDataSource, - getUnmaskedGhostfolioDataSource + transformDataSourceInRequest } from '@ghostfolio/api/helper/data-source.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -28,30 +28,19 @@ export class TransformDataSourceInRequestInterceptor< const http = context.switchToHttp(); const request = http.getRequest(); - if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { - if (request.body?.activities) { - const ghostfolioDataSources = this.configurationService.get( - 'DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER' - ); - - request.body.activities = request.body.activities.map((activity) => { - if (DataSource[activity.dataSource]) { - return { - ...activity, - dataSource: getUnmaskedGhostfolioDataSource({ - ghostfolioDataSources, - dataSource: activity.dataSource - }) - }; - } else { - return { - ...activity, - dataSource: decodeDataSource(activity.dataSource) - }; - } - }); - } + if (request.body?.activities) { + request.body.activities = request.body.activities.map((activity) => { + return { + ...activity, + dataSource: transformDataSourceInRequest({ + configurationService: this.configurationService, + dataSource: activity.dataSource + }) + }; + }); + } + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { for (const type of ['body', 'params', 'query']) { const dataSourceValue = request[type]?.dataSource; @@ -68,19 +57,6 @@ export class TransformDataSourceInRequestInterceptor< }); } } - } else { - if (request.body?.activities) { - request.body.activities = request.body.activities.map((activity) => { - if (DataSource[activity.dataSource]) { - return activity; - } else { - return { - ...activity, - dataSource: decodeDataSource(activity.dataSource) - }; - } - }); - } } return next.handle(); From 265472445c9fc1023e4561513d044cf136ecd345 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:12:58 +0200 Subject: [PATCH 28/35] Task/extend features and pricing page with MCP (#8027) * Add Ghostfolio MCP * Update changelog --- CHANGELOG.md | 4 ++++ .../src/app/pages/features/features-page.html | 23 +++++++++++++++++++ .../src/app/pages/pricing/pricing-page.html | 8 +++++++ 3 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43cd5a98eb..f47f0fec99 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 +### Added + +- Added the server of the Model Context Protocol (MCP) to the features page (experimental) + ### Changed - Improved the label of the cash positions in the holdings charts and table diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index 1682ce12b2..4014d7789d 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -206,6 +206,29 @@
+
+ + +
+

+ Ghostfolio MCP + @if (hasPermissionForSubscription) { + + } + new +

+

+ Let your AI client read your portfolio and import activities + into it via the Model Context Protocol (MCP). +

+
+
+
+
@if (hasPermissionForSubscription) {
diff --git a/apps/client/src/app/pages/pricing/pricing-page.html b/apps/client/src/app/pages/pricing/pricing-page.html index ddd92892a6..cf3bd2448a 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.html +++ b/apps/client/src/app/pages/pricing/pricing-page.html @@ -58,6 +58,10 @@ FIRE Calculator +
  • + + Ghostfolio MCP +
  • Data Import and Export @@ -215,6 +219,10 @@ FIRE Calculator
  • +
  • + + Ghostfolio MCP +
  • Professional Data Provider From 0bee7f5cf1c86d7105fcb743c4627144b2bffef6 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:38:00 +0200 Subject: [PATCH 29/35] Bugfix/interest of account for activities with quantity other than one (#7989) * Fix interest of account for activities with quantity other than one * Update changelog --- CHANGELOG.md | 1 + .../app/portfolio/portfolio.service.spec.ts | 60 +++++++++++++++++++ .../src/app/portfolio/portfolio.service.ts | 2 +- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f47f0fec99..1a5e984fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed the calculation of the interest in the account detail dialog for activities with a quantity other than one - Fixed the positive performance from the all time high in the watchlist - Fixed the portfolio calculation for holdings with historical market prices between the chart dates - Fixed the asset profile identifier in the historical market data gathering of the `POST api/v1/activities` endpoint diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index ccd26e0345..19af3615bc 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -260,6 +260,66 @@ describe('PortfolioService', () => { AND: [whereActivityOfAssetClass, whereActivityOfTag] }); }); + + it('should calculate the dividend and the interest of an account as quantity times unit price', async () => { + jest.spyOn(accountService, 'accounts').mockResolvedValue([ + { + activities: [ + { + currency: 'USD', + date: new Date('2024-01-02'), + quantity: 2, + SymbolProfile: { currency: 'USD' }, + tags: [], + type: 'DIVIDEND', + unitPrice: 10 + }, + { + currency: 'USD', + date: new Date('2024-01-03'), + quantity: 3, + SymbolProfile: { currency: 'USD' }, + tags: [], + type: 'INTEREST', + unitPrice: 5 + } + ], + balance: 0, + currency: 'USD', + id: 'account-id', + name: 'Account' + } + ] as unknown as Awaited>); + + jest.spyOn(portfolioService, 'getDetails').mockResolvedValue({ + accounts: {} + } as unknown as Awaited>); + + jest.spyOn(userService, 'user').mockResolvedValue({ + settings: { settings: { baseCurrency: 'USD' } } + } as unknown as Awaited>); + + jest + .spyOn(exchangeRateDataService, 'toCurrencyAtDate') + .mockImplementation(async (value) => { + return value; + }); + + jest + .spyOn(exchangeRateDataService, 'toCurrency') + .mockImplementation((value) => { + return value; + }); + + const [account] = await portfolioService.getAccounts({ + userId: userDummyData.id + }); + + expect(account).toMatchObject({ + dividendInBaseCurrency: 20, + interestInBaseCurrency: 15 + }); + }); }); describe('getAggregatedMarkets', () => { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 72c5b5946a..d906fd6aef 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -334,7 +334,7 @@ export class PortfolioService { case ActivityType.INTEREST: interestInBaseCurrency += (await this.exchangeRateDataService.toCurrencyAtDate( - unitPrice, + new Big(quantity).mul(unitPrice).toNumber(), currency ?? SymbolProfile.currency, userCurrency, date From 3d34887cbf524367eedbc8a8f7481ff0133c246a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:52:14 +0200 Subject: [PATCH 30/35] Task/improve style in features page (#8029) Improve style --- .../src/app/pages/features/features-page.html | 32 ++++++++++--------- .../src/app/pages/features/features-page.scss | 8 +++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps/client/src/app/pages/features/features-page.html b/apps/client/src/app/pages/features/features-page.html index 4014d7789d..7cc80901f8 100644 --- a/apps/client/src/app/pages/features/features-page.html +++ b/apps/client/src/app/pages/features/features-page.html @@ -137,10 +137,10 @@
    -

    +

    Portfolio Calculations @if (hasPermissionForSubscription) { - + }

    @@ -157,10 +157,10 @@

    -

    +

    Portfolio Allocations @if (hasPermissionForSubscription) { - + }

    @@ -175,10 +175,10 @@

    -

    +

    Static Analysis @if (hasPermissionForSubscription) { - + }

    @@ -193,10 +193,10 @@

    -

    +

    Watchlist @if (hasPermissionForSubscription) { - + }

    @@ -211,12 +211,14 @@

    - Ghostfolio MCP - @if (hasPermissionForSubscription) { - - } + + Ghostfolio MCP + @if (hasPermissionForSubscription) { + + } + new @@ -234,9 +236,9 @@
    -

    +

    Market Mood - +

    Check the current market mood ( Date: Sat, 3 Oct 2026 17:55:14 +0200 Subject: [PATCH 31/35] Task/update locales (#8018) Co-authored-by: github-actions[bot] --- apps/client/src/locales/messages.ca.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.de.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.es.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.fr.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.it.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.ja.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.ko.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.nl.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.pl.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.pt.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.tr.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.uk.xlf | 140 +++++++++++++----------- apps/client/src/locales/messages.xlf | 137 ++++++++++++----------- apps/client/src/locales/messages.zh.xlf | 140 +++++++++++++----------- 14 files changed, 1034 insertions(+), 923 deletions(-) diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 9c541baa59..8fa63a4dc4 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -299,7 +299,7 @@ please apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -599,7 +599,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1043,7 +1043,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -1899,7 +1899,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -1915,7 +1915,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -1983,7 +1983,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2311,7 +2311,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2499,7 +2499,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2519,7 +2519,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2535,7 +2535,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2551,7 +2551,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2563,7 +2563,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2575,11 +2575,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2811,7 +2811,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -3035,7 +3035,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -3792,7 +3792,7 @@ Mode fosc apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -3800,7 +3800,7 @@ Estat d’ànim del mercat apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -3816,7 +3816,7 @@ Multi-idioma apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -3832,7 +3832,7 @@ Programari de codi obert apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -3844,7 +3844,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -3856,7 +3856,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -4544,7 +4544,7 @@ with your university e-mail address apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -5152,7 +5152,7 @@ Looking for a student discount? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -5216,7 +5216,7 @@ here apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -5436,7 +5436,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -5448,7 +5448,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -5460,7 +5460,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -5468,11 +5468,11 @@ Importació i exportació de dades apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -5480,7 +5480,7 @@ Suport comunitari apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -5488,7 +5488,7 @@ S’allotja automàticament, actualitza manualment. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -5496,11 +5496,11 @@ Gratuït apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -5508,7 +5508,7 @@ Per a nous inversors que acaben de començar a operar. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -5516,11 +5516,11 @@ Oferta al núvol Ghostfolio totalment gestionada. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -5528,7 +5528,7 @@ Per a inversors ambiciosos que necessiten la imatge completa dels seus actius financers. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -5548,7 +5548,7 @@ Suport per correu electrònic i xat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -5556,7 +5556,7 @@ Pagament únic, sense renovació automàtica. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -5576,7 +5576,7 @@ És gratuït. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -6033,7 +6033,7 @@ Request it apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6329,7 +6329,7 @@ contact us apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -6409,7 +6409,7 @@ Dipòsit libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -6425,7 +6425,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -6437,7 +6437,7 @@ Estalvi libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -6449,11 +6449,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -6473,7 +6473,7 @@ Mostra-ho tot libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -6721,7 +6721,7 @@ Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -6761,8 +6761,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -7165,7 +7165,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7509,7 +7509,7 @@ If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7641,7 +7641,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8199,11 +8199,11 @@ Change libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ with API access for apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investment diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index ac45a37c37..f11ee375f7 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -42,7 +42,7 @@ bitte apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -230,7 +230,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -570,7 +570,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -818,7 +818,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -830,7 +830,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -994,7 +994,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -1178,11 +1178,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1202,7 +1202,7 @@ Alle anzeigen libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -1402,7 +1402,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -1482,7 +1482,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2258,7 +2258,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2626,7 +2626,7 @@ kontaktiere uns apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -2826,7 +2826,7 @@ Einlage libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -2842,7 +2842,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -2854,7 +2854,7 @@ Ersparnisse libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -3174,8 +3174,8 @@ - Export - exportieren + Export + exportieren libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -3482,7 +3482,7 @@ Suchst du nach einem Studentenrabatt? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -3758,7 +3758,7 @@ Alles von Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3822,7 +3822,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -3838,7 +3838,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -3854,7 +3854,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -3866,11 +3866,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3918,7 +3918,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -3946,7 +3946,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -3958,7 +3958,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -3970,7 +3970,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -3978,7 +3978,7 @@ Selbst gehostet, manuelles Update. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -3986,11 +3986,11 @@ Kostenlos apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -3998,7 +3998,7 @@ Für Einsteiger, die gerade mit dem Börsenhandel beginnen. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -4006,11 +4006,11 @@ Vollständig verwaltetes Ghostfolio Cloud-Angebot. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4018,7 +4018,7 @@ Für ambitionierte Anleger, die den vollständigen Überblick über ihr Anlagevermögen benötigen. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4038,7 +4038,7 @@ Einmalige Zahlung, keine automatische Erneuerung. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4058,7 +4058,7 @@ Es ist kostenlos. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4090,7 +4090,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -4106,11 +4106,11 @@ Datenimport und -export apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4126,7 +4126,7 @@ Community Support apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4134,7 +4134,7 @@ E-Mail und Chat Support apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4194,7 +4194,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -4218,7 +4218,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4646,7 +4646,7 @@ Dark Mode apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4654,7 +4654,7 @@ Marktstimmung apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4670,7 +4670,7 @@ Mehrsprachigkeit apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4686,7 +4686,7 @@ Open Source Software apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -5972,7 +5972,7 @@ mit deiner Universitäts-E-Mail-Adresse apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6168,7 +6168,7 @@ Fordere ihn an apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6616,7 +6616,7 @@ hier apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7189,7 +7189,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7533,7 +7533,7 @@ Wenn du die Eröffnung eines Kontos planst bei apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7665,7 +7665,7 @@ um unseren Empfehlungslink zu verwenden und eine Ghostfolio Premium-Mitgliedschaft für ein Jahr zu erhalten apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8223,11 +8223,11 @@ Änderung libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8255,23 +8255,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8452,7 +8452,7 @@ inklusive API-Zugriff für apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ jemand apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investition diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index b7a2182d07..5ca862e626 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -43,7 +43,7 @@ por favor apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -231,7 +231,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -571,7 +571,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -803,7 +803,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -815,7 +815,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -979,7 +979,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -1163,11 +1163,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1187,7 +1187,7 @@ Mostrar todos libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -1387,7 +1387,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -1467,7 +1467,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2243,7 +2243,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2611,7 +2611,7 @@ contáctanos apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -2803,7 +2803,7 @@ Ahorros libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -2819,7 +2819,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -2831,7 +2831,7 @@ Depósito libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -3159,8 +3159,8 @@ - Export - Exportar + Export + Exportar libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -3459,7 +3459,7 @@ ¿Buscas un descuento para estudiantes? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -3743,7 +3743,7 @@ Todo lo incluido en Basic, más apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3807,7 +3807,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -3823,7 +3823,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -3839,7 +3839,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -3851,11 +3851,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3903,7 +3903,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -3931,7 +3931,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -3943,7 +3943,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -3955,7 +3955,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -3963,7 +3963,7 @@ Autoalojado, actualiza manualmente. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -3971,11 +3971,11 @@ Gratis apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -3983,7 +3983,7 @@ Para nuevos inversores que están empezando con el trading. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -3991,11 +3991,11 @@ Oferta de Ghostfolio en la nube totalmente gestionada. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4003,7 +4003,7 @@ Para inversores ambiciosos que necesitan una visión completa de sus activos financieros. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4023,7 +4023,7 @@ Pago único, sin renovación automática. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4043,7 +4043,7 @@ Es gratis. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4075,7 +4075,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -4091,11 +4091,11 @@ Importación y exportación de datos apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4111,7 +4111,7 @@ Soporte de la comunidad apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4119,7 +4119,7 @@ Soporte a través de correo electrónico y chat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4171,7 +4171,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -4195,7 +4195,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4623,7 +4623,7 @@ Modo oscuro apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4631,7 +4631,7 @@ Sentimiento del mercado apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4647,7 +4647,7 @@ Multilingüe apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4663,7 +4663,7 @@ Software de código abierto apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -5949,7 +5949,7 @@ con tu dirección de correo electrónico universitaria apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6145,7 +6145,7 @@ Solicítalo apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6593,7 +6593,7 @@ aquí apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7166,7 +7166,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7510,7 +7510,7 @@ Si planeas abrir una cuenta en apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7642,7 +7642,7 @@ para utilizar nuestro enlace de recomendación y obtener una membresía de Ghostfolio Premium por un año apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8200,11 +8200,11 @@ Cambio libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8232,23 +8232,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8429,7 +8429,7 @@ con acceso a la API para apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8477,7 +8477,7 @@ alguien apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8792,7 +8792,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8804,7 +8804,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8831,6 +8831,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Inversión diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 9c0cfa5ec9..2a01a5d5dc 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -34,7 +34,7 @@ please apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -294,7 +294,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -642,7 +642,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -1310,7 +1310,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -1470,7 +1470,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -1726,7 +1726,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -1866,7 +1866,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2742,7 +2742,7 @@ Vous cherchez une réduction étudiant ? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -2794,7 +2794,7 @@ Dépôt libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -2934,7 +2934,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -2946,7 +2946,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -3202,7 +3202,7 @@ contact us apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -3250,7 +3250,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -3262,7 +3262,7 @@ Épargne libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -3274,11 +3274,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -3298,7 +3298,7 @@ Montrer tout libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -3450,8 +3450,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -3742,7 +3742,7 @@ Tout ce qui est inclus dans Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3806,7 +3806,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -3822,7 +3822,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -3838,7 +3838,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -3850,11 +3850,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3902,7 +3902,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -3930,7 +3930,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -3942,7 +3942,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -3954,7 +3954,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -3962,7 +3962,7 @@ Hébergé localement, mises à jour manuelles. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -3970,11 +3970,11 @@ Gratuit apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -3982,7 +3982,7 @@ Pour les nouveaux investisseurs qui débutent en Bourse. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -3990,11 +3990,11 @@ Offre Ghostfolio cloud complètement administrée. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4002,7 +4002,7 @@ Pour les investisseurs ambitieux qui ont besoin d’une vue complète de leurs actifs financiers. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4022,7 +4022,7 @@ Paiement unique, sans auto-renouvellement. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4042,7 +4042,7 @@ C’est gratuit. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4074,7 +4074,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -4090,11 +4090,11 @@ Import et Export de Données apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4110,7 +4110,7 @@ Support de la Communauté apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4118,7 +4118,7 @@ Support par E-mail et Tchat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4170,7 +4170,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -4194,7 +4194,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4622,7 +4622,7 @@ Mode Sombre apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4630,7 +4630,7 @@ Sentiment du marché apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4646,7 +4646,7 @@ Multi-Langue apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4662,7 +4662,7 @@ Logiciel Open Source apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -5948,7 +5948,7 @@ avec votre adresse e-mail universitaire apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6144,7 +6144,7 @@ Faites-en la demande apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6592,7 +6592,7 @@ ici apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7165,7 +7165,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7509,7 +7509,7 @@ Si vous prévoyez d’ouvrir un compte chez apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7641,7 +7641,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8199,11 +8199,11 @@ Variation libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ avec accès API pour apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ quelqu’un apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investissement diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 63da41186c..5026711ec5 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -43,7 +43,7 @@ per favore apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -231,7 +231,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -571,7 +571,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -803,7 +803,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -815,7 +815,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -979,7 +979,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -1163,11 +1163,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1187,7 +1187,7 @@ Mostra tutti libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -1387,7 +1387,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -1467,7 +1467,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2243,7 +2243,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2611,7 +2611,7 @@ contact us apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -2803,7 +2803,7 @@ Risparmio libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -2819,7 +2819,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -2831,7 +2831,7 @@ Deposito libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -3159,8 +3159,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -3459,7 +3459,7 @@ Looking for a student discount? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -3743,7 +3743,7 @@ Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3807,7 +3807,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -3823,7 +3823,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -3839,7 +3839,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -3851,11 +3851,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3903,7 +3903,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -3931,7 +3931,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -3943,7 +3943,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -3955,7 +3955,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -3963,7 +3963,7 @@ Self-hosted, aggiornamento manuale. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -3971,11 +3971,11 @@ Free apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -3983,7 +3983,7 @@ Per i nuovi investitori che hanno appena iniziato a fare trading. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -3991,11 +3991,11 @@ Offerta cloud Ghostfolio completamente gestita. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4003,7 +4003,7 @@ Per gli investitori ambiziosi che hanno bisogno di un quadro completo dei propri asset finanziari. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4023,7 +4023,7 @@ Pagamento una tantum, senza rinnovo automatico. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4043,7 +4043,7 @@ È gratuito. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4075,7 +4075,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -4091,11 +4091,11 @@ Importazione ed esportazione dei dati apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4111,7 +4111,7 @@ Supporto della comunità apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4119,7 +4119,7 @@ Supporto via email e chat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4171,7 +4171,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -4195,7 +4195,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4623,7 +4623,7 @@ Modalità scura apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4631,7 +4631,7 @@ Umore del mercato apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4647,7 +4647,7 @@ Multilingue apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4663,7 +4663,7 @@ Software open source apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -5949,7 +5949,7 @@ with your university e-mail address apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6145,7 +6145,7 @@ Request it apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6593,7 +6593,7 @@ here apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7166,7 +7166,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7510,7 +7510,7 @@ If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7642,7 +7642,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8200,11 +8200,11 @@ Cambia libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8232,23 +8232,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8429,7 +8429,7 @@ con accesso API per apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8477,7 +8477,7 @@ qualcuno apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8792,7 +8792,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8804,7 +8804,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8831,6 +8831,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investimento diff --git a/apps/client/src/locales/messages.ja.xlf b/apps/client/src/locales/messages.ja.xlf index bd48a7dda4..a21e0eaa88 100644 --- a/apps/client/src/locales/messages.ja.xlf +++ b/apps/client/src/locales/messages.ja.xlf @@ -244,7 +244,7 @@ お願いします apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -528,7 +528,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -932,7 +932,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -2040,7 +2040,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2224,7 +2224,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2264,7 +2264,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2284,7 +2284,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2300,7 +2300,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2316,7 +2316,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2328,7 +2328,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2340,11 +2340,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2400,7 +2400,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2572,7 +2572,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -2740,7 +2740,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -3444,7 +3444,7 @@ ダークモード apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -3452,7 +3452,7 @@ 市場ムード apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -3468,7 +3468,7 @@ マルチ言語 apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -3484,7 +3484,7 @@ オープンソースソフトウェア apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -3496,7 +3496,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -3508,7 +3508,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -3972,7 +3972,7 @@ Basicのすべて、さらに apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -4188,7 +4188,7 @@ 大学のメールアドレスで apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -4780,7 +4780,7 @@ 学生割引をお探しですか? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -4832,7 +4832,7 @@ 入金 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -5012,7 +5012,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -5024,7 +5024,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -5036,7 +5036,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -5044,11 +5044,11 @@ データのインポートとエクスポート apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -5056,7 +5056,7 @@ コミュニティサポート apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -5064,7 +5064,7 @@ セルフホスト、手動で更新。 apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -5072,11 +5072,11 @@ 無料 apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -5084,7 +5084,7 @@ 取引を始めたばかりの新しい投資家のために。 apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -5092,11 +5092,11 @@ 完全マネージドのGhostfolioクラウドサービス。 apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -5104,7 +5104,7 @@ 自分の金融資産の全体像を必要とする意欲的な投資家のために。 apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -5124,7 +5124,7 @@ メールおよびチャットサポート apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -5140,7 +5140,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -5148,7 +5148,7 @@ 一度きりの支払い、自動更新なし。 apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -5168,7 +5168,7 @@ 無料です。 apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -5617,7 +5617,7 @@ それをリクエスト apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -5793,7 +5793,7 @@ 私たちに連絡 apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -5881,7 +5881,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -5893,7 +5893,7 @@ 貯蓄 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -5905,11 +5905,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -5929,7 +5929,7 @@ すべて表示 libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -6201,8 +6201,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -6641,7 +6641,7 @@ こちら apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7166,7 +7166,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7542,7 +7542,7 @@ If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7666,7 +7666,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8224,11 +8224,11 @@ Change libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8256,23 +8256,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8453,7 +8453,7 @@ with API access for apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8477,7 +8477,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8792,7 +8792,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8804,7 +8804,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8831,6 +8831,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investment diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index 80bf4c9006..76e92c7862 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -244,7 +244,7 @@ 부탁드립니다 apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -528,7 +528,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -932,7 +932,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -2040,7 +2040,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2224,7 +2224,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2264,7 +2264,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2284,7 +2284,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2300,7 +2300,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2316,7 +2316,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2328,7 +2328,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2340,11 +2340,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2400,7 +2400,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2572,7 +2572,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -2740,7 +2740,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -3444,7 +3444,7 @@ 다크 모드 apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -3452,7 +3452,7 @@ 시장 분위기 apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -3468,7 +3468,7 @@ 다국어 apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -3484,7 +3484,7 @@ 오픈 소스 소프트웨어 apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -3496,7 +3496,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -3508,7 +3508,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -4180,7 +4180,7 @@ 대학 이메일 주소로 apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -4772,7 +4772,7 @@ 학생 할인을 찾고 계십니까? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -4824,7 +4824,7 @@ 보증금 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -5004,7 +5004,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -5016,7 +5016,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -5028,7 +5028,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -5036,11 +5036,11 @@ 데이터 가져오기 및 내보내기 apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -5048,7 +5048,7 @@ 커뮤니티 지원 apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -5056,7 +5056,7 @@ 자체 호스팅, 수동으로 업데이트합니다. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -5064,11 +5064,11 @@ 무료 apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -5076,7 +5076,7 @@ 이제 막 거래를 시작한 신규 투자자를 위한 제품입니다. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -5084,11 +5084,11 @@ 완전 관리형 Ghostfolio 클라우드 제품. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -5096,7 +5096,7 @@ 자신의 금융 자산에 대한 전체 그림이 필요한 야심찬 투자자를 위한 제품입니다. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -5116,7 +5116,7 @@ 이메일 및 채팅 지원 apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -5132,7 +5132,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -5140,7 +5140,7 @@ 일회성 결제, 자동 갱신 없음. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -5160,7 +5160,7 @@ 무료입니다. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -5609,7 +5609,7 @@ 요청하세요 apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -5785,7 +5785,7 @@ 저희에게 연락주세요 apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -5873,7 +5873,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -5885,7 +5885,7 @@ 저금 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -5897,11 +5897,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -5921,7 +5921,7 @@ 모두 표시 libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -6161,7 +6161,7 @@ Basic의 모든 기능에 더해 apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -6201,8 +6201,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -6641,7 +6641,7 @@ 여기 apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7166,7 +7166,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7542,7 +7542,7 @@ 에서 계좌를 개설할 계획이라면 apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7666,7 +7666,7 @@ 추천 링크를 사용하고 1년 동안 Ghostfolio 프리미엄 멤버십을 얻으려면 apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8224,11 +8224,11 @@ 변동 libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8256,23 +8256,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8453,7 +8453,7 @@ 다음에 대한 API 액세스 권한이 있는 apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8477,7 +8477,7 @@ 누구 apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8792,7 +8792,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8804,7 +8804,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8831,6 +8831,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment 투자 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index 61da01f6db..6d27beb310 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -42,7 +42,7 @@ alsjeblieft apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -230,7 +230,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -570,7 +570,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -802,7 +802,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -814,7 +814,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -978,7 +978,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -1162,11 +1162,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1186,7 +1186,7 @@ Toon alle libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -1386,7 +1386,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -1466,7 +1466,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2242,7 +2242,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2610,7 +2610,7 @@ contacteer ons apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -2802,7 +2802,7 @@ Besparingen libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -2818,7 +2818,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -2830,7 +2830,7 @@ Storting libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -3158,8 +3158,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -3458,7 +3458,7 @@ Op zoek naar studentenkorting? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -3742,7 +3742,7 @@ Alles van Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3806,7 +3806,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -3822,7 +3822,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -3838,7 +3838,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -3850,11 +3850,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3902,7 +3902,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -3930,7 +3930,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -3942,7 +3942,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -3954,7 +3954,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -3962,7 +3962,7 @@ Zelf hosten, handmatig bijwerken. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -3970,11 +3970,11 @@ Gratis apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -3982,7 +3982,7 @@ Voor nieuwe beleggers die net beginnen met handelen. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -3990,11 +3990,11 @@ Volledig beheerd Ghostfolio cloud-aanbod. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4002,7 +4002,7 @@ Voor ambitieuze beleggers die een volledig beeld willen hebben van hun financiële assets. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4022,7 +4022,7 @@ Eenmalige betaling, geen automatische verlenging. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4042,7 +4042,7 @@ Het is gratis. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4074,7 +4074,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -4090,11 +4090,11 @@ Data Import and Export apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4110,7 +4110,7 @@ Steun van de Gemeenschap apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4118,7 +4118,7 @@ Ondersteuning via e-mail en chat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4170,7 +4170,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -4194,7 +4194,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4622,7 +4622,7 @@ Donker Thema apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4630,7 +4630,7 @@ Marktsentiment apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4646,7 +4646,7 @@ Meerdere talen apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4662,7 +4662,7 @@ Open Source Software apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -5948,7 +5948,7 @@ met uw universitaire e-mailadres apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6144,7 +6144,7 @@ Aanvragen apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6592,7 +6592,7 @@ hier apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7165,7 +7165,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7509,7 +7509,7 @@ Als u van plan bent een rekening te openen bij apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7641,7 +7641,7 @@ om onze referral link te gebruiken en een jaar lang Ghostfolio Premium-lidmaatschap te krijgen apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8199,11 +8199,11 @@ Aanpassen libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ met API toegang tot apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ iemand apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investering diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index f91675891e..c077a7b867 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -243,7 +243,7 @@ proszę apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -519,7 +519,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -923,7 +923,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -2007,7 +2007,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2191,7 +2191,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2231,7 +2231,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2251,7 +2251,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2267,7 +2267,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2283,7 +2283,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2295,7 +2295,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2307,11 +2307,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2367,7 +2367,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2539,7 +2539,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -2707,7 +2707,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -3411,7 +3411,7 @@ Ciemny motyw apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -3419,7 +3419,7 @@ Nastrój rynku apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -3435,7 +3435,7 @@ Wielo-językowość apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -3451,7 +3451,7 @@ Oprogramowanie Open Source apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -3463,7 +3463,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -3475,7 +3475,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -4147,7 +4147,7 @@ używając swojego adresu e-mail uczelni apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -4739,7 +4739,7 @@ Szukasz zniżki studenckiej? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -4791,7 +4791,7 @@ Depozyt libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -4971,7 +4971,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -4983,7 +4983,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -4995,7 +4995,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -5003,11 +5003,11 @@ Importowanie i Eksportowanie Danych apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -5015,7 +5015,7 @@ Wsparcie Społeczności apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -5023,7 +5023,7 @@ Samodzielny hosting, aktualizacja ręczna. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -5031,11 +5031,11 @@ Bezpłatnie apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -5043,7 +5043,7 @@ Dla początkujących inwestorów, którzy dopiero zaczynają swoją przygodę z tradingiem. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -5051,11 +5051,11 @@ W pełni zarządzana oferta Ghostfolio w chmurze. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -5063,7 +5063,7 @@ Dla ambitnych inwestorów, którzy potrzebują pełnego obrazu swoich aktywów finansowych. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -5083,7 +5083,7 @@ Wsparcie przez E-mail i Czat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -5099,7 +5099,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -5107,7 +5107,7 @@ Płatność jednorazowa, bez automatycznego odnawiania. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -5127,7 +5127,7 @@ Jest bezpłatny. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -5564,7 +5564,7 @@ Porpoś o apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -5708,7 +5708,7 @@ skontaktuj się z nami apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -5796,7 +5796,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -5808,7 +5808,7 @@ Oszczędności libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -5820,11 +5820,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -5844,7 +5844,7 @@ Pokaż wszystko libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -6084,7 +6084,7 @@ Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -6124,8 +6124,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -6592,7 +6592,7 @@ tutaj apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7165,7 +7165,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7509,7 +7509,7 @@ Jeśli planujesz otworzyć konto w apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7641,7 +7641,7 @@ aby skorzystać z naszego linku polecającego i otrzymać roczną subskrypcję Ghostfolio Premium apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8199,11 +8199,11 @@ Zmiana libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ z dostępem API dla apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ ktoś apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Inwestycja diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 8181afb0f6..30b412b8d7 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -34,7 +34,7 @@ please apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -294,7 +294,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -642,7 +642,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -1058,7 +1058,7 @@ Depósito libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -1150,7 +1150,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -1310,7 +1310,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -1422,11 +1422,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1446,7 +1446,7 @@ Mostrar tudo libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -1694,7 +1694,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -1794,7 +1794,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2834,7 +2834,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -2846,7 +2846,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -3062,7 +3062,7 @@ contact us apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -3110,7 +3110,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -3122,7 +3122,7 @@ Poupanças libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -3246,8 +3246,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -3542,7 +3542,7 @@ Looking for a student discount? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -3742,7 +3742,7 @@ Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3806,7 +3806,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -3822,7 +3822,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -3838,7 +3838,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -3850,11 +3850,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3902,7 +3902,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -3930,7 +3930,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -3942,7 +3942,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -3954,7 +3954,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -3962,7 +3962,7 @@ Hospedado localmente, atualização manual. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -3970,11 +3970,11 @@ Grátis apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -3982,7 +3982,7 @@ Para novos investidores que estão a começar a investir agora. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -3990,11 +3990,11 @@ Ghostfolio hospedado na nuvem, totalmente gerido. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4002,7 +4002,7 @@ Para investidores ambiciosos que precisam de ter uma visão completa de seus ativos financeiros. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4022,7 +4022,7 @@ Pagamento único, sem renovação automática. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4042,7 +4042,7 @@ É gratuito. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4074,7 +4074,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -4090,11 +4090,11 @@ Importação e Exportação de Dados apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4110,7 +4110,7 @@ Suporte da Comunidade apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4118,7 +4118,7 @@ Suporte por Email e Chat apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4170,7 +4170,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -4194,7 +4194,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4622,7 +4622,7 @@ Modo escuro apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4630,7 +4630,7 @@ Humor do mercado apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4646,7 +4646,7 @@ Multilíngua apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4662,7 +4662,7 @@ Software de código aberto apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -5948,7 +5948,7 @@ with your university e-mail address apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6144,7 +6144,7 @@ Request it apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6592,7 +6592,7 @@ here apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7165,7 +7165,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7509,7 +7509,7 @@ If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7641,7 +7641,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8199,11 +8199,11 @@ Mudar libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ with API access for apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Investment diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index ff324951da..dc518742c1 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -215,7 +215,7 @@ please apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -475,7 +475,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -867,7 +867,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -1847,7 +1847,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2019,7 +2019,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2071,7 +2071,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2091,7 +2091,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2107,7 +2107,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2123,7 +2123,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2135,7 +2135,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2147,11 +2147,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2207,7 +2207,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2883,7 +2883,7 @@ Karanlık Mod apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -2895,7 +2895,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -2903,7 +2903,7 @@ Piyasa Modu apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -2919,7 +2919,7 @@ Çoklu Dil apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -2935,7 +2935,7 @@ Açık Kaynak Yazılım apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -2947,7 +2947,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -2959,7 +2959,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -3979,7 +3979,7 @@ Looking for a student discount? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -4031,7 +4031,7 @@ Para Yatırma libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -4211,7 +4211,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -4223,7 +4223,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -4235,7 +4235,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -4243,11 +4243,11 @@ Veri İçe Aktarma ve Dışa Aktarma apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -4255,7 +4255,7 @@ Topluluk Desteği apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -4263,7 +4263,7 @@ Tarafınızca barındırılıyor, elle güncelleyiniz. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -4271,11 +4271,11 @@ Ücretsiz apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -4283,7 +4283,7 @@ Alım satıma henüz başlamış yeni yatırımcılar için. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -4291,11 +4291,11 @@ Eksiksiz yönetilen Ghostfolio bulut teklifi. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -4303,7 +4303,7 @@ Finansal varlıklarının tamamını görmeye ihtiyaç duyan hırslı yatırımcılar için. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4323,7 +4323,7 @@ E-posta ve Sohbet Desteği apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4339,7 +4339,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -4347,7 +4347,7 @@ Tek seferlik ödeme, otomatik yenileme yok. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4367,7 +4367,7 @@ Ücretsiz. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -4924,7 +4924,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -5244,7 +5244,7 @@ contact us apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -5332,7 +5332,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -5344,7 +5344,7 @@ Tasarruflar libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -5356,11 +5356,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -5380,7 +5380,7 @@ Tümünü göster libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -5620,7 +5620,7 @@ Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -5660,8 +5660,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -5956,7 +5956,7 @@ with your university e-mail address apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -6144,7 +6144,7 @@ Request it apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -6592,7 +6592,7 @@ here apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7165,7 +7165,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7509,7 +7509,7 @@ If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7641,7 +7641,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8199,11 +8199,11 @@ Değişim libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ API erişimi için apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ birisi apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Yatırım diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 133c1b35ac..ede339d5d4 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -299,7 +299,7 @@ будь ласка apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -623,7 +623,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -1031,7 +1031,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -1807,7 +1807,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2023,7 +2023,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2039,7 +2039,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -2083,7 +2083,7 @@ Якщо ви плануєте відкрити рахунок у apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -2131,7 +2131,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2711,7 +2711,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2731,7 +2731,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2747,7 +2747,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2763,7 +2763,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2775,7 +2775,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2787,11 +2787,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -3119,7 +3119,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -3343,7 +3343,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -4096,7 +4096,7 @@ Темний режим apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -4104,7 +4104,7 @@ Ринковий настрій apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -4120,7 +4120,7 @@ Багатомовність apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -4136,7 +4136,7 @@ Програмне забезпечення з відкритим кодом apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -4148,7 +4148,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -4160,7 +4160,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -4848,7 +4848,7 @@ за допомогою вашої університетської електронної адреси apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -5508,7 +5508,7 @@ Шукаєте студентську знижку? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -5572,7 +5572,7 @@ тут apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -5816,7 +5816,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -5828,7 +5828,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -5840,7 +5840,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -5848,11 +5848,11 @@ Імпорт та експорт даних apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -5860,7 +5860,7 @@ Підтримка спільноти apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -5868,7 +5868,7 @@ Самохостинг, оновлення вручну. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -5876,11 +5876,11 @@ Безкоштовно apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -5888,7 +5888,7 @@ Для нових інвесторів, які тільки починають з торгівлі. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -5896,11 +5896,11 @@ Повністю керована хмарна пропозиція Ghostfolio. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -5908,7 +5908,7 @@ Для амбітних інвесторів, яким потрібна повна картина їхніх фінансових активів. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -5928,7 +5928,7 @@ Підтримка електронної пошти та чату apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -5936,7 +5936,7 @@ Разова оплата, без автоматичного поновлення. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -5956,7 +5956,7 @@ Це безкоштовно. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -6287,7 +6287,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6635,7 +6635,7 @@ щоб скористатися нашим реферальним посиланням і отримати підписку Ghostfolio Premium на один рік apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -6867,7 +6867,7 @@ Request it apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -7163,7 +7163,7 @@ зв'яжіться з нами apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -7243,7 +7243,7 @@ Депозит libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -7259,7 +7259,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -7271,7 +7271,7 @@ Заощадження libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -7307,11 +7307,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -7331,7 +7331,7 @@ Показати все libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -7695,7 +7695,7 @@ Усе, що входить у Базовий, плюс apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -7735,8 +7735,8 @@ - Export - Export + Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -8199,11 +8199,11 @@ Змінити libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8231,23 +8231,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8428,7 +8428,7 @@ з доступом до API на apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8476,7 +8476,7 @@ когось apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8791,7 +8791,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8803,7 +8803,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8830,6 +8830,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment Інвестиція diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index 4e1652fc8c..9250c9276d 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -228,7 +228,7 @@ please apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -497,7 +497,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -869,7 +869,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -1868,7 +1868,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2034,7 +2034,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2070,7 +2070,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2089,7 +2089,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2104,7 +2104,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2119,7 +2119,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2130,7 +2130,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2141,11 +2141,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2198,7 +2198,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2353,7 +2353,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -2502,7 +2502,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -3154,14 +3154,14 @@ Dark Mode apps/client/src/app/pages/features/features-page.html - 233 + 258 Market Mood apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -3175,7 +3175,7 @@ Multi-Language apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -3189,7 +3189,7 @@ Open Source Software apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -3200,7 +3200,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -3212,7 +3212,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -3629,7 +3629,7 @@ Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -3818,7 +3818,7 @@ with your university e-mail address apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -4350,7 +4350,7 @@ Looking for a student discount? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -4399,7 +4399,7 @@ Deposit libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -4560,7 +4560,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -4571,7 +4571,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -4582,68 +4582,68 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 Data Import and Export apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 Community Support apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 Self-hosted, update manually. apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 Free apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 For new investors who are just getting started with trading. apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 Fully managed Ghostfolio cloud offering. apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 For ambitious investors who need the full picture of their financial assets. apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -4661,7 +4661,7 @@ Email and Chat Support apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -4676,14 +4676,14 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 One-time payment, no auto-renewal. apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -4701,7 +4701,7 @@ It’s free. apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -5107,7 +5107,7 @@ Request it apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -5264,7 +5264,7 @@ contact us apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -5342,7 +5342,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -5353,7 +5353,7 @@ Savings libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -5364,11 +5364,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -5387,7 +5387,7 @@ Show all libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -5638,7 +5638,7 @@ - Export + Export libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -6031,7 +6031,7 @@ here apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -6502,7 +6502,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -6845,7 +6845,7 @@ If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -6955,7 +6955,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -7456,11 +7456,11 @@ Change libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -7485,23 +7485,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -7661,7 +7661,7 @@ with API access for apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -7682,7 +7682,7 @@ someone apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -7963,7 +7963,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -7974,7 +7974,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -7998,6 +7998,13 @@ 265 + + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 2889ceb609..1f4cc36b00 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -244,7 +244,7 @@ 请 apps/client/src/app/pages/pricing/pricing-page.html - 333 + 341 @@ -528,7 +528,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 113 + 118 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -932,7 +932,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 64 + 69 @@ -2016,7 +2016,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 326 + 334 apps/client/src/app/pages/register/register-page.html @@ -2200,7 +2200,7 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 88 + 93 @@ -2240,7 +2240,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 204 + 208 @@ -2260,7 +2260,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 208 + 212 @@ -2276,7 +2276,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 212 + 216 @@ -2292,7 +2292,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 216 + 220 @@ -2304,7 +2304,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 220 + 228 @@ -2316,11 +2316,11 @@ apps/client/src/app/pages/pricing/pricing-page.html - 75 + 79 apps/client/src/app/pages/pricing/pricing-page.html - 243 + 251 @@ -2376,7 +2376,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 281 + 289 @@ -2548,7 +2548,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 265 + 273 @@ -2716,7 +2716,7 @@ apps/client/src/app/pages/features/features-page.html - 246 + 271 @@ -3420,7 +3420,7 @@ 深色模式 apps/client/src/app/pages/features/features-page.html - 233 + 258 @@ -3428,7 +3428,7 @@ 市场情绪 apps/client/src/app/pages/features/features-page.html - 215 + 240 @@ -3444,7 +3444,7 @@ 多语言 apps/client/src/app/pages/features/features-page.html - 259 + 284 @@ -3460,7 +3460,7 @@ 开源软件 apps/client/src/app/pages/features/features-page.html - 296 + 321 @@ -3472,7 +3472,7 @@ apps/client/src/app/pages/features/features-page.html - 321 + 346 apps/client/src/app/pages/landing/landing-page.html @@ -3484,7 +3484,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 360 + 368 apps/client/src/app/pages/public/public-page.html @@ -3948,7 +3948,7 @@ 包含 Basic 所有功能,以及 apps/client/src/app/pages/pricing/pricing-page.html - 199 + 203 @@ -4164,7 +4164,7 @@ 使用您的学校电子邮件地址 apps/client/src/app/pages/pricing/pricing-page.html - 348 + 356 @@ -4756,7 +4756,7 @@ 寻找学生折扣? apps/client/src/app/pages/pricing/pricing-page.html - 342 + 350 @@ -4808,7 +4808,7 @@ 存款 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 441 + 444 @@ -4988,7 +4988,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 127 + 131 @@ -5000,7 +5000,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 131 + 135 @@ -5012,7 +5012,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 135 + 139 @@ -5020,11 +5020,11 @@ 数据导入与导出 apps/client/src/app/pages/pricing/pricing-page.html - 63 + 67 apps/client/src/app/pages/pricing/pricing-page.html - 139 + 143 @@ -5032,7 +5032,7 @@ 社区支持 apps/client/src/app/pages/pricing/pricing-page.html - 80 + 84 @@ -5040,7 +5040,7 @@ 自托管,手动更新。 apps/client/src/app/pages/pricing/pricing-page.html - 84 + 88 @@ -5048,11 +5048,11 @@ 免费 apps/client/src/app/pages/pricing/pricing-page.html - 86 + 90 apps/client/src/app/pages/pricing/pricing-page.html - 152 + 156 @@ -5060,7 +5060,7 @@ 适合刚开始交易的新投资者。 apps/client/src/app/pages/pricing/pricing-page.html - 119 + 123 @@ -5068,11 +5068,11 @@ 完全托管的 Ghostfolio 云产品。 apps/client/src/app/pages/pricing/pricing-page.html - 150 + 154 apps/client/src/app/pages/pricing/pricing-page.html - 252 + 260 @@ -5080,7 +5080,7 @@ 适合需要全面了解其金融资产的雄心勃勃的投资者。 apps/client/src/app/pages/pricing/pricing-page.html - 193 + 197 @@ -5100,7 +5100,7 @@ 电子邮件和聊天支持 apps/client/src/app/pages/pricing/pricing-page.html - 248 + 256 @@ -5116,7 +5116,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 279 + 287 @@ -5124,7 +5124,7 @@ 一次性付款,无自动续订。 apps/client/src/app/pages/pricing/pricing-page.html - 285 + 293 @@ -5144,7 +5144,7 @@ 免费。 apps/client/src/app/pages/pricing/pricing-page.html - 362 + 370 @@ -5593,7 +5593,7 @@ 请求它 apps/client/src/app/pages/pricing/pricing-page.html - 344 + 352 @@ -5769,7 +5769,7 @@ 联系我们 apps/client/src/app/pages/pricing/pricing-page.html - 336 + 344 @@ -5857,7 +5857,7 @@ libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 451 + 454 libs/ui/src/lib/i18n.ts @@ -5869,7 +5869,7 @@ 储蓄 libs/ui/src/lib/fire-calculator/fire-calculator.component.ts - 461 + 464 @@ -5881,11 +5881,11 @@ libs/ui/src/lib/holdings-table/holdings-table.component.html - 138 + 143 libs/ui/src/lib/holdings-table/holdings-table.component.html - 139 + 144 libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -5905,7 +5905,7 @@ 显示所有 libs/ui/src/lib/holdings-table/holdings-table.component.html - 243 + 248 @@ -6177,8 +6177,8 @@ - Export - 导出 + Export + 导出 libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -6617,7 +6617,7 @@ 这里 apps/client/src/app/pages/pricing/pricing-page.html - 347 + 355 @@ -7166,7 +7166,7 @@ apps/client/src/app/pages/features/features-page.html - 277 + 302 apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts @@ -7510,7 +7510,7 @@ 如果您计划开通账户在 apps/client/src/app/pages/pricing/pricing-page.html - 312 + 320 @@ -7642,7 +7642,7 @@ 使用我们的推荐链接并获得一年的Ghostfolio Premium会员资格 apps/client/src/app/pages/pricing/pricing-page.html - 340 + 348 @@ -8200,11 +8200,11 @@ 涨跌 libs/ui/src/lib/holdings-table/holdings-table.component.html - 160 + 165 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 @@ -8232,23 +8232,23 @@ apps/client/src/app/components/home-overview/home-overview.component.ts - 64 + 65 libs/ui/src/lib/holdings-table/holdings-table.component.html - 185 + 190 libs/ui/src/lib/holdings-table/holdings-table.component.html - 186 + 191 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 421 + 423 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts - 434 + 436 @@ -8429,7 +8429,7 @@ 包含 API 访问权限,适用于 apps/client/src/app/pages/pricing/pricing-page.html - 235 + 243 @@ -8477,7 +8477,7 @@ 某人 apps/client/src/app/pages/public/public-page.component.ts - 68 + 69 @@ -8792,7 +8792,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 294 + 302 @@ -8804,7 +8804,7 @@ apps/client/src/app/pages/pricing/pricing-page.html - 297 + 305 @@ -8831,6 +8831,14 @@ 265 + + new + new + + apps/client/src/app/pages/features/features-page.html + 223 + + Investment 投资 From 6fe67fb692e072802409a5df16ae88a34e33e56b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 17:58:09 +0200 Subject: [PATCH 32/35] Task/improve language localization for DE (20261003) (#8030) * Update translations * Update changelog --- CHANGELOG.md | 1 + apps/client/src/locales/messages.de.xlf | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a5e984fb2..ae11a3f51f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Excluded the cash position in the base currency from the holdings table on the overview tab of the home page (experimental) - Extended the tools to get the activities, the portfolio and the watchlist in the server of the Model Context Protocol (MCP) to include the data source (experimental) - Removed the deprecated `SymbolProfile` field from the endpoints `GET api/v1/activities`, `GET api/v1/activities/:id` and `POST api/v1/import` +- Improved the language localization for German (`de`) - Upgraded `@openrouter/ai-sdk-provider` from version `3.0.0` to `3.1.0` - Upgraded `ai` from version `7.0.37` to `7.0.114` - Upgraded `dotenv` from version `17.4.2` to `18.0.3` diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index f11ee375f7..fabe62f3f2 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -3175,7 +3175,7 @@ Export - exportieren + exportieren libs/ui/src/lib/activities-table/activities-table.component.html 72 @@ -8832,7 +8832,7 @@ new - new + neu apps/client/src/app/pages/features/features-page.html 223 From 0d6457bf7eba14842d9a50094e7caa9b91b22508 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:00:07 +0200 Subject: [PATCH 33/35] Release 3.78.0 (#8031) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae11a3f51f..d674289d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 +## 3.78.0 - 2026-10-03 ### Added diff --git a/package-lock.json b/package-lock.json index be96328456..d0096a72d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "3.77.0", + "version": "3.78.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "3.77.0", + "version": "3.78.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 7a32e9bdf5..b19db0a4bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "3.77.0", + "version": "3.78.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 48a3fbf3709d99c2c6445589d5c586e5019e60c1 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 18:03:02 +0200 Subject: [PATCH 34/35] Bugfix/year in validation message of date (#8028) Fix year in validation message of date --- libs/common/src/lib/validator-constraints/is-after-1970.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/common/src/lib/validator-constraints/is-after-1970.ts b/libs/common/src/lib/validator-constraints/is-after-1970.ts index d55dc67c54..a3ba006308 100644 --- a/libs/common/src/lib/validator-constraints/is-after-1970.ts +++ b/libs/common/src/lib/validator-constraints/is-after-1970.ts @@ -1,5 +1,6 @@ import { isValidDateAfter1970 } from '@ghostfolio/common/helper'; +import { utc } from '@date-fns/utc'; import { ValidatorConstraint, ValidatorConstraintInterface @@ -9,7 +10,7 @@ import { format } from 'date-fns'; @ValidatorConstraint({ name: 'isAfter1970' }) export class IsAfter1970Constraint implements ValidatorConstraintInterface { public defaultMessage() { - return `date must be after ${format(new Date(0), 'yyyy')}`; + return `date must be after ${format(new Date(0), 'yyyy', { in: utc })}`; } public validate(aDate: Date | string) { From 5fac38c4d78e45c1d8631f40b42979c2a48cd576 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:04:30 +0200 Subject: [PATCH 35/35] Task/setup tests for valid and invalid import files (#8021) Setup tests for valid and invalid import files --- .../src/app/import/import-data.dto.spec.ts | 73 +++++++++++++++++++ apps/api/tsconfig.spec.json | 1 + 2 files changed, 74 insertions(+) create mode 100644 apps/api/src/app/import/import-data.dto.spec.ts diff --git a/apps/api/src/app/import/import-data.dto.spec.ts b/apps/api/src/app/import/import-data.dto.spec.ts new file mode 100644 index 0000000000..b6cce2a739 --- /dev/null +++ b/apps/api/src/app/import/import-data.dto.spec.ts @@ -0,0 +1,73 @@ +import { BadRequestException, ValidationPipe } from '@nestjs/common'; +import { DataSource, Type } from '@prisma/client'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { ImportDataDto } from './import-data.dto'; + +const validationPipe = new ValidationPipe({ + forbidNonWhitelisted: true, + transform: true, + whitelist: true +}); + +function loadImportData(directory: string, fileName: string): ImportDataDto { + const { accounts, activities, assetProfiles, platforms, tags } = JSON.parse( + readFileSync( + join(__dirname, '../../../../../test/import', directory, fileName), + 'utf8' + ) + ) as ImportDataDto; + + return { accounts, activities, assetProfiles, platforms, tags }; +} + +describe('ImportDataDto', () => { + it('accepts sample.json', async () => { + await expect( + validationPipe.transform(loadImportData('ok', 'sample.json'), { + metatype: ImportDataDto, + type: 'body' + }) + ).resolves.toBeInstanceOf(ImportDataDto); + }); + + it.each([ + { + fileName: 'invalid-data-source.json', + messages: [ + `activities.0.dataSource must be one of the following values: ${Object.values(DataSource).join(', ')}` + ] + }, + { + fileName: 'invalid-date-before-min.json', + messages: ['activities.0.date must be after 1970'] + }, + { + fileName: 'invalid-date.json', + messages: [ + 'activities.0.date must be after 1970', + 'activities.0.date must be a valid ISO 8601 date string' + ] + }, + { + fileName: 'invalid-type.json', + messages: [ + `activities.0.type must be one of the following values: ${Object.values(Type).join(', ')}` + ] + } + ])('refuses $fileName', async ({ fileName, messages }) => { + const error: unknown = await validationPipe + .transform(loadImportData('not-ok', fileName), { + metatype: ImportDataDto, + type: 'body' + }) + .catch((validationError: unknown) => validationError); + + expect(error).toBeInstanceOf(BadRequestException); + + expect((error as BadRequestException).getResponse()).toMatchObject({ + message: messages + }); + }); +}); diff --git a/apps/api/tsconfig.spec.json b/apps/api/tsconfig.spec.json index e5f982722c..63aa6d4f95 100644 --- a/apps/api/tsconfig.spec.json +++ b/apps/api/tsconfig.spec.json @@ -2,6 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", + "target": "es2021", "module": "preserve", "types": ["jest", "node"] },