From 6b167a917e5cf55fba39b7b9e301ae3a45bd8f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Wed, 28 Jan 2026 15:32:09 +0100 Subject: [PATCH 01/13] Add calculation and display of annualized dividend yield based on dividends received in the last 12 months relative to the investment cost basis. Implementation: - Add annualizedDividendYield property to TimelinePosition, PortfolioSnapshot, and PortfolioSummary interfaces - Calculate individual position yield based on last 12 months dividends / investment with currency effects - Aggregate portfolio-wide yield in PortfolioSnapshot - Extract and include yield in PortfolioSummary via PortfolioService Tests: - Add comprehensive tests with single position (MSFT) - Add multi-position test (MSFT + IBM) to verify aggregation - Add PortfolioService integration tests - Add IBM mock data to CurrentRateService - Update cash test to include new property --- .../calculator/portfolio-calculator.ts | 29 ++ .../roai/portfolio-calculator-cash.spec.ts | 1 + ...-calculator-msft-buy-with-dividend.spec.ts | 348 +++++++++++++++++- .../calculator/roai/portfolio-calculator.ts | 15 + .../portfolio/current-rate.service.mock.ts | 9 + .../app/portfolio/portfolio.service.spec.ts | 77 ++++ .../src/app/portfolio/portfolio.service.ts | 8 +- .../interfaces/portfolio-summary.interface.ts | 1 + .../src/lib/models/portfolio-snapshot.ts | 1 + .../src/lib/models/timeline-position.ts | 1 + 10 files changed, 487 insertions(+), 3 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio.service.spec.ts diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 2e58a4ef5..e87ec0e51 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -48,6 +48,7 @@ import { eachYearOfInterval, endOfDay, endOfYear, + subYears, format, isAfter, isBefore, @@ -185,6 +186,7 @@ export abstract class PortfolioCalculator { if (!transactionPoints.length) { return { activitiesCount: 0, + annualizedDividendYield: 0, createdAt: new Date(), currentValueInBaseCurrency: new Big(0), errors: [], @@ -403,11 +405,38 @@ export abstract class PortfolioCalculator { }; } + // Calculate annualized dividend yield based on investment (cost basis) + const twelveMonthsAgo = subYears(this.endDate, 1); + const dividendsLast12Months = this.activities + .filter(({ SymbolProfile, type, date }) => { + return ( + SymbolProfile.symbol === item.symbol && + type === 'DIVIDEND' && + new Date(date) >= twelveMonthsAgo && + new Date(date) <= this.endDate + ); + }) + .reduce((sum, activity) => { + const exchangeRate = + exchangeRatesByCurrency[ + `${activity.SymbolProfile.currency}${this.currency}` + ]?.[format(new Date(activity.date), DATE_FORMAT)] ?? 1; + const dividendAmount = activity.quantity.mul(activity.unitPrice); + return sum.plus(dividendAmount.mul(exchangeRate)); + }, new Big(0)); + + const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0) + ? dividendsLast12Months + .div(totalInvestmentWithCurrencyEffect) + .toNumber() + : 0; + positions.push({ includeInTotalAssetValue, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, activitiesCount: item.activitiesCount, + annualizedDividendYield, averagePrice: item.averagePrice, currency: item.currency, dataSource: item.dataSource, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index a53ebcf05..fc046eb98 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -231,6 +231,7 @@ describe('PortfolioCalculator', () => { */ expect(position).toMatchObject({ activitiesCount: 2, + annualizedDividendYield: 0, averagePrice: new Big(1), currency: 'USD', dataSource: DataSource.YAHOO, 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 b19adb642..70e6771b5 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 @@ -79,7 +79,7 @@ describe('PortfolioCalculator', () => { }); describe('get current positions', () => { - it.only('with MSFT buy', async () => { + it('with MSFT buy', async () => { jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); const activities: Activity[] = [ @@ -178,5 +178,351 @@ describe('PortfolioCalculator', () => { }) ); }); + + it('with MSFT buy and four quarterly dividends to calculate annualized dividend yield', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 19, + feeInBaseCurrency: 19, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 298.58 + }, + { + ...activityDummyData, + date: new Date('2022-08-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.62 + }, + { + ...activityDummyData, + date: new Date('2022-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.68 + }, + { + ...activityDummyData, + date: new Date('2023-02-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.68 + }, + { + ...activityDummyData, + date: new Date('2023-05-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.68 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + expect(portfolioSnapshot).toMatchObject({ + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 5, + averagePrice: new Big('298.58'), + currency: 'USD', + dataSource: 'YAHOO', + dateOfFirstActivity: '2021-09-16', + dividend: new Big('2.66'), + dividendInBaseCurrency: new Big('2.66'), + fee: new Big('19'), + firstBuyDate: '2021-09-16', + quantity: new Big('1'), + symbol: 'MSFT', + tags: [], + transactionCount: 5 + } + ] + }); + + const position = portfolioSnapshot.positions[0]; + expect(position).toHaveProperty('annualizedDividendYield'); + expect(position.annualizedDividendYield).toBeGreaterThan(0); + + // Verify that the snapshot data is sufficient for portfolio summary calculation + // Portfolio summary annualized dividend yield = totalDividend / totalInvestment + const expectedPortfolioYield = new Big(position.dividendInBaseCurrency) + .div(position.investmentWithCurrencyEffect) + .toNumber(); + + expect(position.annualizedDividendYield).toBeCloseTo( + expectedPortfolioYield, + 10 + ); + expect(expectedPortfolioYield).toBeCloseTo(0.00891, 3); // ~0.89% yield on cost + }); + + it('with MSFT and IBM positions to verify portfolio-wide dividend yield aggregation', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); + + const activities: Activity[] = [ + // MSFT: 1 share @ 300, 4 quarterly dividends = 2.60 total + { + ...activityDummyData, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 300 + }, + { + ...activityDummyData, + date: new Date('2022-08-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2022-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2023-02-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2023-05-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + // IBM: 1 share @ 200, 4 quarterly dividends = 6.60 total + { + ...activityDummyData, + date: new Date('2021-10-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 200 + }, + { + ...activityDummyData, + date: new Date('2022-09-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2022-12-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2023-03-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2023-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + expect(portfolioSnapshot.positions).toHaveLength(2); + + const msftPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'MSFT' + ); + const ibmPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'IBM' + ); + + // MSFT: 2.60 dividends / 300 investment = 0.00867 (0.867%) + expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('2.6')); + expect(msftPosition.investmentWithCurrencyEffect).toEqual(new Big('300')); + expect(msftPosition.annualizedDividendYield).toBeCloseTo(2.6 / 300, 5); + + // IBM: 6.60 dividends / 200 investment = 0.033 (3.3%) + expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('6.6')); + expect(ibmPosition.investmentWithCurrencyEffect).toEqual(new Big('200')); + expect(ibmPosition.annualizedDividendYield).toBeCloseTo(6.6 / 200, 5); + + // Portfolio-wide: (2.60 + 6.60) / (300 + 200) = 9.20 / 500 = 0.0184 (1.84%) + const totalDividends = new Big(msftPosition.dividendInBaseCurrency).plus( + ibmPosition.dividendInBaseCurrency + ); + const totalInvestment = new Big( + msftPosition.investmentWithCurrencyEffect + ).plus(ibmPosition.investmentWithCurrencyEffect); + + expect(totalDividends.toNumber()).toBe(9.2); + expect(totalInvestment.toNumber()).toBe(500); + + // Test that portfolioSnapshot has aggregated annualizedDividendYield + expect(portfolioSnapshot).toHaveProperty('annualizedDividendYield'); + expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo(0.0184, 4); + }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index fe912510a..8f29c618e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -34,6 +34,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let grossPerformanceWithCurrencyEffect = new Big(0); let hasErrors = false; let netPerformance = new Big(0); + let totalDividendsInBaseCurrency = new Big(0); let totalFeesWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); @@ -46,6 +47,12 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { return includeInTotalAssetValue; } )) { + if (currentPosition.dividendInBaseCurrency) { + totalDividendsInBaseCurrency = totalDividendsInBaseCurrency.plus( + currentPosition.dividendInBaseCurrency + ); + } + if (currentPosition.feeInBaseCurrency) { totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( currentPosition.feeInBaseCurrency @@ -105,6 +112,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } + // Calculate annualized dividend yield for the entire portfolio + const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0) + ? totalDividendsInBaseCurrency + .div(totalInvestmentWithCurrencyEffect) + .toNumber() + : 0; + return { currentValueInBaseCurrency, hasErrors, @@ -116,6 +130,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { activitiesCount: this.activities.filter(({ type }) => { return ['BUY', 'SELL'].includes(type); }).length, + annualizedDividendYield, createdAt: new Date(), errors: [], historicalData: [], 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 4b4b8f00e..f4e76a44d 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -64,6 +64,15 @@ function mockGetValue(symbol: string, date: Date) { return { marketPrice: 0 }; + case 'IBM': + if (isSameDay(parseDate('2021-10-01'), date)) { + return { marketPrice: 140.5 }; + } else if (isSameDay(parseDate('2023-07-10'), date)) { + return { marketPrice: 145.2 }; + } + + return { marketPrice: 0 }; + case 'MSFT': if (isSameDay(parseDate('2021-09-16'), date)) { return { marketPrice: 89.12 }; diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts new file mode 100644 index 000000000..ae45cb931 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -0,0 +1,77 @@ +import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; + +describe('PortfolioService', () => { + describe('getSummary', () => { + it('should include annualizedDividendYield from calculator snapshot', async () => { + // This test verifies that getSummary() correctly extracts + // annualizedDividendYield from the calculator snapshot + // and includes it in the returned PortfolioSummary + + // Mock calculator with annualizedDividendYield in snapshot + const mockSnapshot = { + annualizedDividendYield: 0.0184, // 1.84% + currentValueInBaseCurrency: { toNumber: () => 500 }, + totalInvestment: { toNumber: () => 500 }, + totalInvestmentWithCurrencyEffect: { toNumber: () => 500 } + }; + + const mockCalculator = { + getSnapshot: jest.fn().mockResolvedValue(mockSnapshot) + } as unknown as PortfolioCalculator; + + // Verify that the snapshot has the annualizedDividendYield + const snapshot = await mockCalculator.getSnapshot(); + expect(snapshot).toHaveProperty('annualizedDividendYield'); + expect(snapshot.annualizedDividendYield).toBe(0.0184); + + // The actual PortfolioService.getSummary() implementation should: + // 1. Call portfolioCalculator.getSnapshot() + // 2. Extract annualizedDividendYield from the snapshot + // 3. Include it in the returned PortfolioSummary + // + // Implementation in portfolio.service.ts:1867-1869: + // const { annualizedDividendYield, ... } = await portfolioCalculator.getSnapshot(); + // + // And in the return statement at line 1965: + // return { annualizedDividendYield, ... } + }); + + it('should handle zero annualizedDividendYield for portfolios without dividends', async () => { + const mockSnapshot = { + annualizedDividendYield: 0, + currentValueInBaseCurrency: { toNumber: () => 1000 }, + totalInvestment: { toNumber: () => 1000 }, + totalInvestmentWithCurrencyEffect: { toNumber: () => 1000 } + }; + + const mockCalculator = { + getSnapshot: jest.fn().mockResolvedValue(mockSnapshot) + } as unknown as PortfolioCalculator; + + const snapshot = await mockCalculator.getSnapshot(); + expect(snapshot.annualizedDividendYield).toBe(0); + }); + + it('should verify the data flow from Calculator to Service', () => { + // This test documents the expected data flow: + // + // 1. Calculator Level (portfolio-calculator.ts): + // - Calculates annualizedDividendYield for each position + // - Aggregates to portfolio-wide annualizedDividendYield in snapshot + // + // 2. Service Level (portfolio.service.ts:getSummary): + // - Calls: const { annualizedDividendYield } = await portfolioCalculator.getSnapshot() + // - Returns: { annualizedDividendYield, ...otherFields } + // + // 3. API Response (PortfolioSummary interface): + // - Client receives annualizedDividendYield as part of the summary + // + // This flow is verified by: + // - Calculator tests: portfolio-calculator-msft-buy-with-dividend.spec.ts + // - This service test: verifies extraction from snapshot + // - Integration would be tested via E2E tests (if they existed) + + expect(true).toBe(true); // Documentation test + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7be375473..4052c3515 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1860,8 +1860,11 @@ export class PortfolioService { } } - const { currentValueInBaseCurrency, totalInvestment } = - await portfolioCalculator.getSnapshot(); + const { + annualizedDividendYield, + currentValueInBaseCurrency, + totalInvestment + } = await portfolioCalculator.getSnapshot(); const { performance } = await this.getPerformance({ impersonationId, @@ -1959,6 +1962,7 @@ export class PortfolioService { })?.toNumber(); return { + annualizedDividendYield, annualizedPerformancePercent, annualizedPerformancePercentWithCurrencyEffect, cash, diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index ccf94dcf7..3db88761a 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -3,6 +3,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface'; export interface PortfolioSummary extends PortfolioPerformance { activityCount: number; + annualizedDividendYield: number; annualizedPerformancePercent: number; annualizedPerformancePercentWithCurrencyEffect: number; cash: number; diff --git a/libs/common/src/lib/models/portfolio-snapshot.ts b/libs/common/src/lib/models/portfolio-snapshot.ts index 6b13ca048..f4cad3bdc 100644 --- a/libs/common/src/lib/models/portfolio-snapshot.ts +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -10,6 +10,7 @@ import { Transform, Type } from 'class-transformer'; export class PortfolioSnapshot { activitiesCount: number; + annualizedDividendYield: number; createdAt: Date; diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 13f9001d5..058cd8ac3 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -10,6 +10,7 @@ import { Transform, Type } from 'class-transformer'; export class TimelinePosition { activitiesCount: number; + annualizedDividendYield: number; @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) From 0e260b9bc9b10a1184f9e0e02e755d7596d024af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Wed, 28 Jan 2026 16:39:15 +0100 Subject: [PATCH 02/13] Feature: Add annualized dividend yield to portfolio summary UI - Display annualized dividend yield as indented entry below Dividend - Only visible when experimental features are enabled --- .../portfolio-summary.component.html | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 46eb2845c..0ea84e84c 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -374,4 +374,21 @@ /> + @if (user?.settings?.isExperimentalFeatures) { +
+
+ Annualized Dividend Yield +
+
+ +
+
+ } From d331e9e49c5acf2cbf0610c5a2a1f836c9657186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Wed, 28 Jan 2026 16:41:42 +0100 Subject: [PATCH 03/13] add i18n --- apps/client/src/locales/messages.ca.xlf | 8 ++++++++ apps/client/src/locales/messages.de.xlf | 8 ++++++++ apps/client/src/locales/messages.es.xlf | 8 ++++++++ apps/client/src/locales/messages.fr.xlf | 8 ++++++++ apps/client/src/locales/messages.it.xlf | 8 ++++++++ apps/client/src/locales/messages.ko.xlf | 8 ++++++++ apps/client/src/locales/messages.nl.xlf | 8 ++++++++ apps/client/src/locales/messages.pl.xlf | 8 ++++++++ apps/client/src/locales/messages.pt.xlf | 8 ++++++++ apps/client/src/locales/messages.tr.xlf | 8 ++++++++ apps/client/src/locales/messages.uk.xlf | 8 ++++++++ apps/client/src/locales/messages.xlf | 7 +++++++ apps/client/src/locales/messages.zh.xlf | 8 ++++++++ 13 files changed, 103 insertions(+) diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index a6bec7df3..45b6866a2 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -2274,6 +2274,14 @@ 216 + + Annualized Dividend Yield + Rendiment de dividends anualitzat + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Proveïdor de dades professional diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index 44ee607c3..b7cf14920 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -3757,6 +3757,14 @@ 34 + + Annualized Dividend Yield + Annualisierte Dividendenrendite + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Professioneller Datenanbieter diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 7564e4d80..6ed792d62 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -3734,6 +3734,14 @@ 32 + + Annualized Dividend Yield + Rendimiento de dividendos anualizado + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Proveedor de datos profesional diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index 0da1c3f6a..d97abbcac 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -3733,6 +3733,14 @@ 32 + + Annualized Dividend Yield + Rendement de dividende annualisé + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Fournisseur de Données Professionnel diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index 746e1fbd1..f78e2a3df 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -3734,6 +3734,14 @@ 32 + + Annualized Dividend Yield + Rendimento da dividendi annualizzato + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Fornitore di dati professionale diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index 67443706b..fe0d92fd5 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -2035,6 +2035,14 @@ 216 + + Annualized Dividend Yield + 연간 배당 수익률 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider 전문 데이터 제공자 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index d7d0b71e8..f865c4707 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -3733,6 +3733,14 @@ 32 + + Annualized Dividend Yield + Geannualiseerd dividendrendement + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Professionele gegevensleverancier diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index a94a76d2f..bf8992687 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -2002,6 +2002,14 @@ 216 + + Annualized Dividend Yield + Roczna stopa dywidendy + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Profesjonalne źródło danych diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 2bcd7c401..788a2aa7e 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -3733,6 +3733,14 @@ 32 + + Annualized Dividend Yield + Rendimento de dividendos anualizado + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Provedor de Dados Profissionais diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index 421ef3855..cd515c861 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -1870,6 +1870,14 @@ 216 + + Annualized Dividend Yield + Yıllık Temettü Getirisi + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Profesyonel Veri Sağlayıcı diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index 2fc389030..de461f76a 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -2478,6 +2478,14 @@ 216 + + Annualized Dividend Yield + Річна дивідендна дохідність + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider Професійний постачальник даних diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index a6907698d..64c91b108 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -1867,6 +1867,13 @@ 216 + + Annualized Dividend Yield + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index 7ec9d85a0..cb60c8550 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -2011,6 +2011,14 @@ 216 + + Annualized Dividend Yield + 年化股息收益率 + + apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html + 379 + + Professional Data Provider 专业数据提供商 From 4940ce11fd757c229fdb69ff2be2a7016da604d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Wed, 28 Jan 2026 16:43:37 +0100 Subject: [PATCH 04/13] changelog update --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ddad0b5..f264473e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 2.234.0 - 2026-01-30 +### Added + +- Added the annualized dividend yield to the portfolio summary (experimental) + ### Changed - Improved the usability of the create asset profile dialog in the market data section of the admin control panel From b1a15409f9972a53b879c43f0fada76ab9e3b130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Thu, 29 Jan 2026 08:48:59 +0100 Subject: [PATCH 05/13] fix yearly boundary --- ...-calculator-msft-buy-with-dividend.spec.ts | 163 ++++++++++++++++++ .../calculator/roai/portfolio-calculator.ts | 15 +- 2 files changed, 172 insertions(+), 6 deletions(-) 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 70e6771b5..e08fa1f4b 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 @@ -524,5 +524,168 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot).toHaveProperty('annualizedDividendYield'); expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo(0.0184, 4); }); + + it('ignores dividends older than 12 months when aggregating portfolio yield', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); + + const activities: Activity[] = [ + // MSFT: 1 share @ 300, 3 dividends total (one older than 12 months) + { + ...activityDummyData, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 300 + }, + { + ...activityDummyData, + date: new Date('2021-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.62 + }, + { + ...activityDummyData, + date: new Date('2022-08-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2023-05-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + // IBM: 1 share @ 200, 2 dividends total (one older than 12 months) + { + ...activityDummyData, + date: new Date('2021-10-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 200 + }, + { + ...activityDummyData, + date: new Date('2022-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2023-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const msftPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'MSFT' + ); + const ibmPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'IBM' + ); + + expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('1.92')); + expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('3.3')); + + const msftDividendLast12Months = new Big('1.3'); + const ibmDividendLast12Months = new Big('1.65'); + const totalInvestment = new Big('500'); + + expect(msftPosition.annualizedDividendYield).toBeCloseTo( + msftDividendLast12Months.div(new Big('300')).toNumber(), + 6 + ); + expect(ibmPosition.annualizedDividendYield).toBeCloseTo( + ibmDividendLast12Months.div(new Big('200')).toNumber(), + 6 + ); + + const expectedAnnualizedDividendYield = msftDividendLast12Months + .plus(ibmDividendLast12Months) + .div(totalInvestment) + .toNumber(); + + expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo( + expectedAnnualizedDividendYield, + 6 + ); + }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index 8f29c618e..a971d1c8e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -34,7 +34,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let grossPerformanceWithCurrencyEffect = new Big(0); let hasErrors = false; let netPerformance = new Big(0); - let totalDividendsInBaseCurrency = new Big(0); + let totalDividendsLast12MonthsInBaseCurrency = new Big(0); let totalFeesWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); @@ -47,10 +47,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { return includeInTotalAssetValue; } )) { - if (currentPosition.dividendInBaseCurrency) { - totalDividendsInBaseCurrency = totalDividendsInBaseCurrency.plus( - currentPosition.dividendInBaseCurrency - ); + if (currentPosition.investmentWithCurrencyEffect) { + totalDividendsLast12MonthsInBaseCurrency = + totalDividendsLast12MonthsInBaseCurrency.plus( + new Big(currentPosition.annualizedDividendYield ?? 0).mul( + currentPosition.investmentWithCurrencyEffect + ) + ); } if (currentPosition.feeInBaseCurrency) { @@ -114,7 +117,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { // Calculate annualized dividend yield for the entire portfolio const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0) - ? totalDividendsInBaseCurrency + ? totalDividendsLast12MonthsInBaseCurrency .div(totalInvestmentWithCurrencyEffect) .toNumber() : 0; From 8b4cad9f197c4afdf5e4064a090bb8c75e114af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Thu, 29 Jan 2026 08:59:08 +0100 Subject: [PATCH 06/13] Replaced the placeholder spec with a real unit test that instantiates PortfolioService, stubs dependencies, calls getSummary, and asserts the returned summary fields (including annualizedDividendYield and computed totals). The new test lives in apps/api/src/app/portfolio/portfolio.service.spec.ts. --- .../app/portfolio/portfolio.service.spec.ts | 221 +++++++++++++----- 1 file changed, 160 insertions(+), 61 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index ae45cb931..3fe6438dd 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -1,77 +1,176 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; +import { + activityDummyData, + symbolProfileDummyData, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; +import { Activity } from '@ghostfolio/common/interfaces'; +import { RequestWithUser } from '@ghostfolio/common/types'; + +import { Type as ActivityType } from '@prisma/client'; +import { Big } from 'big.js'; + +import { PortfolioService } from './portfolio.service'; describe('PortfolioService', () => { describe('getSummary', () => { - it('should include annualizedDividendYield from calculator snapshot', async () => { - // This test verifies that getSummary() correctly extracts - // annualizedDividendYield from the calculator snapshot - // and includes it in the returned PortfolioSummary - - // Mock calculator with annualizedDividendYield in snapshot - const mockSnapshot = { - annualizedDividendYield: 0.0184, // 1.84% - currentValueInBaseCurrency: { toNumber: () => 500 }, - totalInvestment: { toNumber: () => 500 }, - totalInvestmentWithCurrencyEffect: { toNumber: () => 500 } + beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date('2023-07-10')); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); + }); + + it('returns annualizedDividendYield from the calculator snapshot', async () => { + const activities: Activity[] = [ + { + ...activityDummyData, + currency: 'USD', + date: new Date('2023-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 2, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: ActivityType.BUY, + unitPrice: 50, + unitPriceInAssetProfileCurrency: 50, + value: 100, + valueInBaseCurrency: 100 + }, + { + ...activityDummyData, + currency: 'USD', + date: new Date('2023-06-02'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: ActivityType.SELL, + unitPrice: 40, + unitPriceInAssetProfileCurrency: 40, + value: 40, + valueInBaseCurrency: 40 + } + ]; + + const exchangeRateDataService = { + toCurrency: jest.fn((value: number) => value) }; - const mockCalculator = { - getSnapshot: jest.fn().mockResolvedValue(mockSnapshot) - } as unknown as PortfolioCalculator; + const orderService = { + getOrders: jest.fn().mockResolvedValue({ activities }) + }; - // Verify that the snapshot has the annualizedDividendYield - const snapshot = await mockCalculator.getSnapshot(); - expect(snapshot).toHaveProperty('annualizedDividendYield'); - expect(snapshot.annualizedDividendYield).toBe(0.0184); - - // The actual PortfolioService.getSummary() implementation should: - // 1. Call portfolioCalculator.getSnapshot() - // 2. Extract annualizedDividendYield from the snapshot - // 3. Include it in the returned PortfolioSummary - // - // Implementation in portfolio.service.ts:1867-1869: - // const { annualizedDividendYield, ... } = await portfolioCalculator.getSnapshot(); - // - // And in the return statement at line 1965: - // return { annualizedDividendYield, ... } - }); + const userService = { + user: jest.fn().mockResolvedValue({ + id: userDummyData.id, + settings: { + settings: { + baseCurrency: DEFAULT_CURRENCY, + emergencyFund: 0 + } + } + }) + }; + + const accountService = { + getCashDetails: jest.fn().mockResolvedValue({ + balanceInBaseCurrency: 1000 + }) + }; - it('should handle zero annualizedDividendYield for portfolios without dividends', async () => { - const mockSnapshot = { - annualizedDividendYield: 0, - currentValueInBaseCurrency: { toNumber: () => 1000 }, - totalInvestment: { toNumber: () => 1000 }, - totalInvestmentWithCurrencyEffect: { toNumber: () => 1000 } + const impersonationService = { + validateImpersonationId: jest.fn().mockResolvedValue(undefined) }; - const mockCalculator = { - getSnapshot: jest.fn().mockResolvedValue(mockSnapshot) + const request = { + user: { + id: userDummyData.id, + settings: { settings: { baseCurrency: DEFAULT_CURRENCY } } + } + } as RequestWithUser; + + const portfolioCalculator = { + getDividendInBaseCurrency: jest.fn().mockResolvedValue(new Big(12)), + getFeesInBaseCurrency: jest.fn().mockResolvedValue(new Big(4)), + getInterestInBaseCurrency: jest.fn().mockResolvedValue(new Big(1)), + getLiabilitiesInBaseCurrency: jest.fn().mockResolvedValue(new Big(6)), + getSnapshot: jest.fn().mockResolvedValue({ + annualizedDividendYield: 0.0123, + currentValueInBaseCurrency: new Big(500), + totalInvestment: new Big(400) + }), + getStartDate: jest.fn().mockReturnValue(new Date('2023-01-01')) } as unknown as PortfolioCalculator; - const snapshot = await mockCalculator.getSnapshot(); - expect(snapshot.annualizedDividendYield).toBe(0); - }); + const service = new PortfolioService( + {} as any, + accountService as any, + {} as any, + {} as any, + {} as any, + exchangeRateDataService as any, + {} as any, + impersonationService as any, + orderService as any, + request, + {} as any, + {} as any, + userService as any + ); + + jest.spyOn(service, 'getPerformance').mockResolvedValue({ + performance: { + netPerformance: 20, + netPerformancePercentage: 0.05, + netPerformancePercentageWithCurrencyEffect: 0.05, + netPerformanceWithCurrencyEffect: 20 + } + } as any); + + const summary = await (service as any).getSummary({ + balanceInBaseCurrency: 1000, + emergencyFundHoldingsValueInBaseCurrency: 0, + filteredValueInBaseCurrency: new Big(200), + impersonationId: userDummyData.id, + portfolioCalculator, + userCurrency: DEFAULT_CURRENCY, + userId: userDummyData.id + }); - it('should verify the data flow from Calculator to Service', () => { - // This test documents the expected data flow: - // - // 1. Calculator Level (portfolio-calculator.ts): - // - Calculates annualizedDividendYield for each position - // - Aggregates to portfolio-wide annualizedDividendYield in snapshot - // - // 2. Service Level (portfolio.service.ts:getSummary): - // - Calls: const { annualizedDividendYield } = await portfolioCalculator.getSnapshot() - // - Returns: { annualizedDividendYield, ...otherFields } - // - // 3. API Response (PortfolioSummary interface): - // - Client receives annualizedDividendYield as part of the summary - // - // This flow is verified by: - // - Calculator tests: portfolio-calculator-msft-buy-with-dividend.spec.ts - // - This service test: verifies extraction from snapshot - // - Integration would be tested via E2E tests (if they existed) - - expect(true).toBe(true); // Documentation test + expect(portfolioCalculator.getSnapshot).toHaveBeenCalledTimes(1); + expect(summary).toMatchObject({ + annualizedDividendYield: 0.0123, + cash: 1000, + committedFunds: 60, + dividendInBaseCurrency: 12, + fees: 4, + grossPerformance: 24, + grossPerformanceWithCurrencyEffect: 24, + interestInBaseCurrency: 1, + liabilitiesInBaseCurrency: 6, + totalBuy: 100, + totalInvestment: 400, + totalSell: 40, + totalValueInBaseCurrency: 1494 + }); + expect(summary.activityCount).toBe(2); + expect(summary.dateOfFirstActivity).toEqual(new Date('2023-01-01')); }); }); }); From 2c75ccc0bc4ddc831d97924039ded6dea11bbc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Wed, 4 Feb 2026 08:39:35 +0100 Subject: [PATCH 07/13] remove firstBuyDate in test --- .../roai/portfolio-calculator-msft-buy-with-dividend.spec.ts | 1 - 1 file changed, 1 deletion(-) 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 e08fa1f4b..e2f1deec2 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 @@ -287,7 +287,6 @@ describe('PortfolioCalculator', () => { dividend: new Big('2.66'), dividendInBaseCurrency: new Big('2.66'), fee: new Big('19'), - firstBuyDate: '2021-09-16', quantity: new Big('1'), symbol: 'MSFT', tags: [], From 86b5bef84967407a955aad336cd0fecba96372e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Fri, 6 Feb 2026 17:04:52 +0100 Subject: [PATCH 08/13] renaming annualizedDividendYield --- CLAUDE.md | 119 ++++++++++++++++++ .../calculator/portfolio-calculator.ts | 33 +++-- .../roai/portfolio-calculator-cash.spec.ts | 2 +- ...-calculator-msft-buy-with-dividend.spec.ts | 33 +++-- .../calculator/roai/portfolio-calculator.ts | 23 ++-- .../interfaces/portfolio-order.interface.ts | 1 + .../app/portfolio/portfolio.service.spec.ts | 6 +- .../src/app/portfolio/portfolio.service.ts | 4 +- .../portfolio-summary.component.html | 10 +- .../interfaces/portfolio-summary.interface.ts | 2 +- .../src/lib/models/portfolio-snapshot.ts | 2 +- .../src/lib/models/timeline-position.ts | 2 +- 12 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..8ea8b51a0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,119 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Ghostfolio is an open source wealth management software built with TypeScript in an Nx monorepo workspace. It's a full-stack application with Angular frontend and NestJS backend, using PostgreSQL with Prisma ORM and Redis for caching. + +## Development Commands + +### Environment Setup + +```bash +npm install +docker compose -f docker/docker-compose.dev.yml up -d # Start PostgreSQL and Redis +npm run database:setup # Initialize database schema +``` + +### Development Servers + +```bash +npm run start:server # Start NestJS API server +npm run start:client # Start Angular client (English) +npm run watch:server # Start server in watch mode for debugging +``` + +### Build and Production + +```bash +npm run build:production # Build both API and client for production +npm run start:production # Run production build with database migration +``` + +### Database Operations + +```bash +npm run database:push # Sync schema with database (development) +npm run database:migrate # Apply migrations (production) +npm run database:seed # Seed database with initial data +npm run database:gui # Open Prisma Studio +npm run database:format-schema # Format Prisma schema +npm run database:generate-typings # Generate Prisma client +``` + +### Testing and Quality + +```bash +npm test # Run all tests (API + common) +npm run test:api # Run API tests only +npm run test:common # Run common library tests +npm run test:single # Run single test file (example provided) +npm run lint # Run ESLint on all projects +npm run format # Format code with Prettier +npm run format:check # Check code formatting +``` + +### Nx Workspace Commands + +```bash +nx affected:build # Build affected projects +nx affected:test # Test affected projects +nx affected:lint # Lint affected projects +nx dep-graph # View dependency graph +``` + +### Storybook (Component Library) + +```bash +npm run start:storybook # Start Storybook development server +npm run build:storybook # Build Storybook for production +``` + +## Architecture + +### Monorepo Structure + +- **apps/api**: NestJS backend application +- **apps/client**: Angular frontend application +- **apps/client-e2e**: E2E tests for client +- **apps/ui-e2e**: E2E tests for UI components +- **libs/common**: Shared TypeScript libraries and utilities +- **libs/ui**: Angular UI component library + +### Technology Stack + +- **Frontend**: Angular 20 with Angular Material, Bootstrap utility classes +- **Backend**: NestJS with TypeScript +- **Database**: PostgreSQL with Prisma ORM +- **Caching**: Redis with Bull for job queues +- **Build Tool**: Nx workspace +- **Testing**: Jest for unit tests, Cypress for E2E tests + +### Key Dependencies + +- **Authentication**: Passport (JWT, Google OAuth, WebAuthn) +- **Data Sources**: Yahoo Finance, CoinGecko APIs for market data +- **Charts**: Chart.js with various plugins +- **Payment**: Stripe integration +- **Internationalization**: Angular i18n with multiple language support + +### Database Schema + +The Prisma schema defines models for: + +- User management and access control +- Account and portfolio tracking +- Trading activities and orders +- Market data and asset information +- Platform integrations + +### Development Notes + +- Node.js version >=22.18.0 required +- Uses Nx generators for consistent code scaffolding +- Husky for git hooks and code quality enforcement +- Environment files: `.env.dev` for development, `.env.example` as template +- SSL certificates can be generated for localhost development +- Experimental features can be toggled via user settings +- always run the .husky pre commit hooks after code changes diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index e87ec0e51..e76dcfd58 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -119,6 +119,7 @@ export abstract class PortfolioCalculator { this.activities = activities .map( ({ + currency, date, feeInAssetProfileCurrency, feeInBaseCurrency, @@ -139,6 +140,7 @@ export abstract class PortfolioCalculator { } return { + currency, SymbolProfile, tags, type, @@ -186,7 +188,7 @@ export abstract class PortfolioCalculator { if (!transactionPoints.length) { return { activitiesCount: 0, - annualizedDividendYield: 0, + dividendYieldTrailingTwelveMonths: 0, createdAt: new Date(), currentValueInBaseCurrency: new Big(0), errors: [], @@ -405,38 +407,43 @@ export abstract class PortfolioCalculator { }; } - // Calculate annualized dividend yield based on investment (cost basis) + // Calculate dividend yield based on trailing twelve months of dividends and investment (cost basis) const twelveMonthsAgo = subYears(this.endDate, 1); const dividendsLast12Months = this.activities .filter(({ SymbolProfile, type, date }) => { return ( SymbolProfile.symbol === item.symbol && type === 'DIVIDEND' && - new Date(date) >= twelveMonthsAgo && - new Date(date) <= this.endDate + isWithinInterval(new Date(date), { + start: twelveMonthsAgo, + end: this.endDate + }) ); }) .reduce((sum, activity) => { + const activityCurrency = + activity.currency ?? activity.SymbolProfile.currency; const exchangeRate = - exchangeRatesByCurrency[ - `${activity.SymbolProfile.currency}${this.currency}` - ]?.[format(new Date(activity.date), DATE_FORMAT)] ?? 1; + exchangeRatesByCurrency[`${activityCurrency}${this.currency}`]?.[ + format(new Date(activity.date), DATE_FORMAT) + ] ?? 1; const dividendAmount = activity.quantity.mul(activity.unitPrice); return sum.plus(dividendAmount.mul(exchangeRate)); }, new Big(0)); - const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0) - ? dividendsLast12Months - .div(totalInvestmentWithCurrencyEffect) - .toNumber() - : 0; + const dividendYieldTrailingTwelveMonths = + totalInvestmentWithCurrencyEffect.gt(0) + ? dividendsLast12Months + .div(totalInvestmentWithCurrencyEffect) + .toNumber() + : 0; positions.push({ includeInTotalAssetValue, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, activitiesCount: item.activitiesCount, - annualizedDividendYield, + dividendYieldTrailingTwelveMonths, averagePrice: item.averagePrice, currency: item.currency, dataSource: item.dataSource, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index fc046eb98..d9cb4f808 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -231,7 +231,7 @@ describe('PortfolioCalculator', () => { */ expect(position).toMatchObject({ activitiesCount: 2, - annualizedDividendYield: 0, + dividendYieldTrailingTwelveMonths: 0, averagePrice: new Big(1), currency: 'USD', dataSource: DataSource.YAHOO, 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 e2f1deec2..1725ef046 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 @@ -296,8 +296,8 @@ describe('PortfolioCalculator', () => { }); const position = portfolioSnapshot.positions[0]; - expect(position).toHaveProperty('annualizedDividendYield'); - expect(position.annualizedDividendYield).toBeGreaterThan(0); + expect(position).toHaveProperty('dividendYieldTrailingTwelveMonths'); + expect(position.dividendYieldTrailingTwelveMonths).toBeGreaterThan(0); // Verify that the snapshot data is sufficient for portfolio summary calculation // Portfolio summary annualized dividend yield = totalDividend / totalInvestment @@ -305,7 +305,7 @@ describe('PortfolioCalculator', () => { .div(position.investmentWithCurrencyEffect) .toNumber(); - expect(position.annualizedDividendYield).toBeCloseTo( + expect(position.dividendYieldTrailingTwelveMonths).toBeCloseTo( expectedPortfolioYield, 10 ); @@ -501,12 +501,18 @@ describe('PortfolioCalculator', () => { // MSFT: 2.60 dividends / 300 investment = 0.00867 (0.867%) expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('2.6')); expect(msftPosition.investmentWithCurrencyEffect).toEqual(new Big('300')); - expect(msftPosition.annualizedDividendYield).toBeCloseTo(2.6 / 300, 5); + expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( + 2.6 / 300, + 5 + ); // IBM: 6.60 dividends / 200 investment = 0.033 (3.3%) expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('6.6')); expect(ibmPosition.investmentWithCurrencyEffect).toEqual(new Big('200')); - expect(ibmPosition.annualizedDividendYield).toBeCloseTo(6.6 / 200, 5); + expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( + 6.6 / 200, + 5 + ); // Portfolio-wide: (2.60 + 6.60) / (300 + 200) = 9.20 / 500 = 0.0184 (1.84%) const totalDividends = new Big(msftPosition.dividendInBaseCurrency).plus( @@ -519,9 +525,14 @@ describe('PortfolioCalculator', () => { expect(totalDividends.toNumber()).toBe(9.2); expect(totalInvestment.toNumber()).toBe(500); - // Test that portfolioSnapshot has aggregated annualizedDividendYield - expect(portfolioSnapshot).toHaveProperty('annualizedDividendYield'); - expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo(0.0184, 4); + // Test that portfolioSnapshot has aggregated dividendYieldTrailingTwelveMonths + expect(portfolioSnapshot).toHaveProperty( + 'dividendYieldTrailingTwelveMonths' + ); + expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( + 0.0184, + 4 + ); }); it('ignores dividends older than 12 months when aggregating portfolio yield', async () => { @@ -667,11 +678,11 @@ describe('PortfolioCalculator', () => { const ibmDividendLast12Months = new Big('1.65'); const totalInvestment = new Big('500'); - expect(msftPosition.annualizedDividendYield).toBeCloseTo( + expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( msftDividendLast12Months.div(new Big('300')).toNumber(), 6 ); - expect(ibmPosition.annualizedDividendYield).toBeCloseTo( + expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( ibmDividendLast12Months.div(new Big('200')).toNumber(), 6 ); @@ -681,7 +692,7 @@ describe('PortfolioCalculator', () => { .div(totalInvestment) .toNumber(); - expect(portfolioSnapshot.annualizedDividendYield).toBeCloseTo( + expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( expectedAnnualizedDividendYield, 6 ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index a971d1c8e..b20200766 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -34,7 +34,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let grossPerformanceWithCurrencyEffect = new Big(0); let hasErrors = false; let netPerformance = new Big(0); - let totalDividendsLast12MonthsInBaseCurrency = new Big(0); + let totalDividendsTrailingTwelveMonthsInBaseCurrency = new Big(0); let totalFeesWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); @@ -48,9 +48,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } )) { if (currentPosition.investmentWithCurrencyEffect) { - totalDividendsLast12MonthsInBaseCurrency = - totalDividendsLast12MonthsInBaseCurrency.plus( - new Big(currentPosition.annualizedDividendYield ?? 0).mul( + totalDividendsTrailingTwelveMonthsInBaseCurrency = + totalDividendsTrailingTwelveMonthsInBaseCurrency.plus( + new Big(currentPosition.dividendYieldTrailingTwelveMonths ?? 0).mul( currentPosition.investmentWithCurrencyEffect ) ); @@ -115,12 +115,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } - // Calculate annualized dividend yield for the entire portfolio - const annualizedDividendYield = totalInvestmentWithCurrencyEffect.gt(0) - ? totalDividendsLast12MonthsInBaseCurrency - .div(totalInvestmentWithCurrencyEffect) - .toNumber() - : 0; + // Calculate dividend yield for the entire portfolio based on trailing twelve months + const dividendYieldTrailingTwelveMonths = + totalInvestmentWithCurrencyEffect.gt(0) + ? totalDividendsTrailingTwelveMonthsInBaseCurrency + .div(totalInvestmentWithCurrencyEffect) + .toNumber() + : 0; return { currentValueInBaseCurrency, @@ -133,7 +134,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { activitiesCount: this.activities.filter(({ type }) => { return ['BUY', 'SELL'].includes(type); }).length, - annualizedDividendYield, + dividendYieldTrailingTwelveMonths, createdAt: new Date(), errors: [], historicalData: [], diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 2dbd68f12..96e116e9a 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -1,6 +1,7 @@ import { Activity } from '@ghostfolio/common/interfaces'; export interface PortfolioOrder extends Pick { + currency?: string; date: string; fee: Big; feeInBaseCurrency: Big; diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 3fe6438dd..3c7a293c5 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -24,7 +24,7 @@ describe('PortfolioService', () => { jest.restoreAllMocks(); }); - it('returns annualizedDividendYield from the calculator snapshot', async () => { + it('returns dividendYieldTrailingTwelveMonths from the calculator snapshot', async () => { const activities: Activity[] = [ { ...activityDummyData, @@ -111,7 +111,7 @@ describe('PortfolioService', () => { getInterestInBaseCurrency: jest.fn().mockResolvedValue(new Big(1)), getLiabilitiesInBaseCurrency: jest.fn().mockResolvedValue(new Big(6)), getSnapshot: jest.fn().mockResolvedValue({ - annualizedDividendYield: 0.0123, + dividendYieldTrailingTwelveMonths: 0.0123, currentValueInBaseCurrency: new Big(500), totalInvestment: new Big(400) }), @@ -155,7 +155,7 @@ describe('PortfolioService', () => { expect(portfolioCalculator.getSnapshot).toHaveBeenCalledTimes(1); expect(summary).toMatchObject({ - annualizedDividendYield: 0.0123, + dividendYieldTrailingTwelveMonths: 0.0123, cash: 1000, committedFunds: 60, dividendInBaseCurrency: 12, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 4052c3515..a288dc60f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1861,7 +1861,7 @@ export class PortfolioService { } const { - annualizedDividendYield, + dividendYieldTrailingTwelveMonths, currentValueInBaseCurrency, totalInvestment } = await portfolioCalculator.getSnapshot(); @@ -1962,7 +1962,7 @@ export class PortfolioService { })?.toNumber(); return { - annualizedDividendYield, + dividendYieldTrailingTwelveMonths, annualizedPerformancePercent, annualizedPerformancePercentWithCurrencyEffect, cash, diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 0ea84e84c..116690c09 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -376,8 +376,10 @@ @if (user?.settings?.isExperimentalFeatures) {
-
- Annualized Dividend Yield +
+ Dividend Yield (Trailing Twelve Months)
diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index 3db88761a..532870791 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -3,7 +3,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface'; export interface PortfolioSummary extends PortfolioPerformance { activityCount: number; - annualizedDividendYield: number; + dividendYieldTrailingTwelveMonths: number; annualizedPerformancePercent: number; annualizedPerformancePercentWithCurrencyEffect: number; cash: number; diff --git a/libs/common/src/lib/models/portfolio-snapshot.ts b/libs/common/src/lib/models/portfolio-snapshot.ts index f4cad3bdc..33bab356c 100644 --- a/libs/common/src/lib/models/portfolio-snapshot.ts +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -10,7 +10,7 @@ import { Transform, Type } from 'class-transformer'; export class PortfolioSnapshot { activitiesCount: number; - annualizedDividendYield: number; + dividendYieldTrailingTwelveMonths: number; createdAt: Date; diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 058cd8ac3..ba4a93482 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -10,7 +10,7 @@ import { Transform, Type } from 'class-transformer'; export class TimelinePosition { activitiesCount: number; - annualizedDividendYield: number; + dividendYieldTrailingTwelveMonths: number; @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) From 955b8177952d744001fe988a7cb33320911305d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Fri, 6 Feb 2026 17:13:37 +0100 Subject: [PATCH 09/13] efactor test file --- ...culator-dividend-yield-multi-asset.spec.ts | 468 ++++++++++++++++++ ...-calculator-msft-buy-with-dividend.spec.ts | 402 +-------------- 2 files changed, 485 insertions(+), 385 deletions(-) create mode 100644 apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-dividend-yield-multi-asset.spec.ts diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-dividend-yield-multi-asset.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-dividend-yield-multi-asset.spec.ts new file mode 100644 index 000000000..87bb40c61 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-dividend-yield-multi-asset.spec.ts @@ -0,0 +1,468 @@ +import { + activityDummyData, + symbolProfileDummyData, + 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(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('Multi-asset dividend yield', () => { + it('with MSFT and IBM positions verifies portfolio-wide dividend yield aggregation', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); + + const activities: Activity[] = [ + // MSFT: 1 share @ 300, 4 quarterly dividends = 2.60 total + { + ...activityDummyData, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 300 + }, + { + ...activityDummyData, + date: new Date('2022-08-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2022-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2023-02-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2023-05-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + // IBM: 1 share @ 200, 4 quarterly dividends = 6.60 total + { + ...activityDummyData, + date: new Date('2021-10-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 200 + }, + { + ...activityDummyData, + date: new Date('2022-09-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2022-12-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2023-03-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2023-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + expect(portfolioSnapshot.positions).toHaveLength(2); + + const msftPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'MSFT' + ); + const ibmPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'IBM' + ); + + // MSFT: 2.60 dividends / 300 investment = 0.00867 (0.867%) + expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('2.6')); + expect(msftPosition.investmentWithCurrencyEffect).toEqual(new Big('300')); + expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( + 2.6 / 300, + 5 + ); + + // IBM: 6.60 dividends / 200 investment = 0.033 (3.3%) + expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('6.6')); + expect(ibmPosition.investmentWithCurrencyEffect).toEqual(new Big('200')); + expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( + 6.6 / 200, + 5 + ); + + // Portfolio-wide: (2.60 + 6.60) / (300 + 200) = 9.20 / 500 = 0.0184 (1.84%) + const totalDividends = new Big(msftPosition.dividendInBaseCurrency).plus( + ibmPosition.dividendInBaseCurrency + ); + const totalInvestment = new Big( + msftPosition.investmentWithCurrencyEffect + ).plus(ibmPosition.investmentWithCurrencyEffect); + + expect(totalDividends.toNumber()).toBe(9.2); + expect(totalInvestment.toNumber()).toBe(500); + + // Verify portfolio-level dividend yield aggregation + expect(portfolioSnapshot).toHaveProperty( + 'dividendYieldTrailingTwelveMonths' + ); + expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( + 0.0184, + 4 + ); + }); + + it('ignores dividends older than 12 months when aggregating portfolio yield', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); + + const activities: Activity[] = [ + // MSFT: 1 share @ 300, 3 dividends total (one older than 12 months) + { + ...activityDummyData, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 300 + }, + { + ...activityDummyData, + date: new Date('2021-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.62 + }, + { + ...activityDummyData, + date: new Date('2022-08-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + { + ...activityDummyData, + date: new Date('2023-05-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'Microsoft Inc.', + symbol: 'MSFT' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 0.65 + }, + // IBM: 1 share @ 200, 2 dividends total (one older than 12 months) + { + ...activityDummyData, + date: new Date('2021-10-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'BUY', + unitPriceInAssetProfileCurrency: 200 + }, + { + ...activityDummyData, + date: new Date('2022-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + }, + { + ...activityDummyData, + date: new Date('2023-06-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: 'YAHOO', + name: 'IBM', + symbol: 'IBM' + }, + type: 'DIVIDEND', + unitPriceInAssetProfileCurrency: 1.65 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const msftPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'MSFT' + ); + const ibmPosition = portfolioSnapshot.positions.find( + ({ symbol }) => symbol === 'IBM' + ); + + expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('1.92')); + expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('3.3')); + + const msftDividendLast12Months = new Big('1.3'); + const ibmDividendLast12Months = new Big('1.65'); + const totalInvestment = new Big('500'); + + expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( + msftDividendLast12Months.div(new Big('300')).toNumber(), + 6 + ); + expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( + ibmDividendLast12Months.div(new Big('200')).toNumber(), + 6 + ); + + const expectedDividendYield = msftDividendLast12Months + .plus(ibmDividendLast12Months) + .div(totalInvestment) + .toNumber(); + + expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( + expectedDividendYield, + 6 + ); + }); + }); +}); 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 1725ef046..e82f9ee4f 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 @@ -292,409 +292,41 @@ describe('PortfolioCalculator', () => { tags: [], transactionCount: 5 } - ] + ], + totalFeesWithCurrencyEffect: new Big('19'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('298.58'), + totalInvestmentWithCurrencyEffect: new Big('298.58'), + totalLiabilitiesWithCurrencyEffect: new Big('0') }); + // Verify position-level dividend yield const position = portfolioSnapshot.positions[0]; expect(position).toHaveProperty('dividendYieldTrailingTwelveMonths'); expect(position.dividendYieldTrailingTwelveMonths).toBeGreaterThan(0); - // Verify that the snapshot data is sufficient for portfolio summary calculation - // Portfolio summary annualized dividend yield = totalDividend / totalInvestment - const expectedPortfolioYield = new Big(position.dividendInBaseCurrency) + const expectedPositionYield = new Big(position.dividendInBaseCurrency) .div(position.investmentWithCurrencyEffect) .toNumber(); - expect(position.dividendYieldTrailingTwelveMonths).toBeCloseTo( - expectedPortfolioYield, + expectedPositionYield, 10 ); - expect(expectedPortfolioYield).toBeCloseTo(0.00891, 3); // ~0.89% yield on cost - }); - - it('with MSFT and IBM positions to verify portfolio-wide dividend yield aggregation', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); - - const activities: Activity[] = [ - // MSFT: 1 share @ 300, 4 quarterly dividends = 2.60 total - { - ...activityDummyData, - date: new Date('2021-09-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'BUY', - unitPriceInAssetProfileCurrency: 300 - }, - { - ...activityDummyData, - date: new Date('2022-08-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.65 - }, - { - ...activityDummyData, - date: new Date('2022-11-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.65 - }, - { - ...activityDummyData, - date: new Date('2023-02-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.65 - }, - { - ...activityDummyData, - date: new Date('2023-05-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.65 - }, - // IBM: 1 share @ 200, 4 quarterly dividends = 6.60 total - { - ...activityDummyData, - date: new Date('2021-10-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'BUY', - unitPriceInAssetProfileCurrency: 200 - }, - { - ...activityDummyData, - date: new Date('2022-09-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 1.65 - }, - { - ...activityDummyData, - date: new Date('2022-12-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 1.65 - }, - { - ...activityDummyData, - date: new Date('2023-03-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 1.65 - }, - { - ...activityDummyData, - date: new Date('2023-06-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 1.65 - } - ]; - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: 'USD', - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - expect(portfolioSnapshot.positions).toHaveLength(2); - - const msftPosition = portfolioSnapshot.positions.find( - ({ symbol }) => symbol === 'MSFT' - ); - const ibmPosition = portfolioSnapshot.positions.find( - ({ symbol }) => symbol === 'IBM' - ); - - // MSFT: 2.60 dividends / 300 investment = 0.00867 (0.867%) - expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('2.6')); - expect(msftPosition.investmentWithCurrencyEffect).toEqual(new Big('300')); - expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( - 2.6 / 300, - 5 - ); - - // IBM: 6.60 dividends / 200 investment = 0.033 (3.3%) - expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('6.6')); - expect(ibmPosition.investmentWithCurrencyEffect).toEqual(new Big('200')); - expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( - 6.6 / 200, - 5 - ); - - // Portfolio-wide: (2.60 + 6.60) / (300 + 200) = 9.20 / 500 = 0.0184 (1.84%) - const totalDividends = new Big(msftPosition.dividendInBaseCurrency).plus( - ibmPosition.dividendInBaseCurrency - ); - const totalInvestment = new Big( - msftPosition.investmentWithCurrencyEffect - ).plus(ibmPosition.investmentWithCurrencyEffect); + expect(expectedPositionYield).toBeCloseTo(0.00891, 3); // ~0.89% yield on cost - expect(totalDividends.toNumber()).toBe(9.2); - expect(totalInvestment.toNumber()).toBe(500); - - // Test that portfolioSnapshot has aggregated dividendYieldTrailingTwelveMonths + // Verify portfolio-level dividend yield expect(portfolioSnapshot).toHaveProperty( 'dividendYieldTrailingTwelveMonths' ); expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( - 0.0184, - 4 - ); - }); - - it('ignores dividends older than 12 months when aggregating portfolio yield', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime()); - - const activities: Activity[] = [ - // MSFT: 1 share @ 300, 3 dividends total (one older than 12 months) - { - ...activityDummyData, - date: new Date('2021-09-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'BUY', - unitPriceInAssetProfileCurrency: 300 - }, - { - ...activityDummyData, - date: new Date('2021-11-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.62 - }, - { - ...activityDummyData, - date: new Date('2022-08-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.65 - }, - { - ...activityDummyData, - date: new Date('2023-05-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'Microsoft Inc.', - symbol: 'MSFT' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 0.65 - }, - // IBM: 1 share @ 200, 2 dividends total (one older than 12 months) - { - ...activityDummyData, - date: new Date('2021-10-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'BUY', - unitPriceInAssetProfileCurrency: 200 - }, - { - ...activityDummyData, - date: new Date('2022-06-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 1.65 - }, - { - ...activityDummyData, - date: new Date('2023-06-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'USD', - dataSource: 'YAHOO', - name: 'IBM', - symbol: 'IBM' - }, - type: 'DIVIDEND', - unitPriceInAssetProfileCurrency: 1.65 - } - ]; - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: 'USD', - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const msftPosition = portfolioSnapshot.positions.find( - ({ symbol }) => symbol === 'MSFT' - ); - const ibmPosition = portfolioSnapshot.positions.find( - ({ symbol }) => symbol === 'IBM' - ); - - expect(msftPosition.dividendInBaseCurrency).toEqual(new Big('1.92')); - expect(ibmPosition.dividendInBaseCurrency).toEqual(new Big('3.3')); - - const msftDividendLast12Months = new Big('1.3'); - const ibmDividendLast12Months = new Big('1.65'); - const totalInvestment = new Big('500'); - - expect(msftPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( - msftDividendLast12Months.div(new Big('300')).toNumber(), - 6 - ); - expect(ibmPosition.dividendYieldTrailingTwelveMonths).toBeCloseTo( - ibmDividendLast12Months.div(new Big('200')).toNumber(), - 6 + expectedPositionYield, + 10 ); - const expectedAnnualizedDividendYield = msftDividendLast12Months - .plus(ibmDividendLast12Months) - .div(totalInvestment) - .toNumber(); - - expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( - expectedAnnualizedDividendYield, - 6 + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( + expect.objectContaining({ + totalInvestmentValueWithCurrencyEffect: 298.58 + }) ); }); }); From 8320deed684e5ff6885f2b74137446073f587e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Fri, 6 Feb 2026 17:30:55 +0100 Subject: [PATCH 10/13] alphabetize date-fns imports in portfolio-calculator --- apps/api/src/app/portfolio/calculator/portfolio-calculator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index e76dcfd58..67a21ab33 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -48,14 +48,14 @@ import { eachYearOfInterval, endOfDay, endOfYear, - subYears, format, isAfter, isBefore, isWithinInterval, min, startOfYear, - subDays + subDays, + subYears } from 'date-fns'; import { isNumber, sortBy, sum, uniqBy } from 'lodash'; From 8834c1c1b10aeaaecd27ed2ec1d6fb7ab3128fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Mon, 9 Feb 2026 15:41:42 +0100 Subject: [PATCH 11/13] fix minor issues from feedback --- CHANGELOG.md | 10 +- CLAUDE.md | 119 ------------------ .../calculator/portfolio-calculator.ts | 4 +- .../roai/portfolio-calculator-cash.spec.ts | 2 +- ...-calculator-msft-buy-with-dividend.spec.ts | 3 +- .../calculator/roai/portfolio-calculator.ts | 2 +- .../src/app/portfolio/portfolio.service.ts | 4 +- .../interfaces/portfolio-summary.interface.ts | 2 +- .../src/lib/models/portfolio-snapshot.ts | 3 +- .../src/lib/models/timeline-position.ts | 3 +- 10 files changed, 18 insertions(+), 134 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f264473e4..fa8cd79e8 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 + +### Added + +- Added the dividend yield (trailing twelve months) to the portfolio summary (experimental) + ## 2.237.0 - 2026-02-08 ### Changed @@ -48,10 +54,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 2.234.0 - 2026-01-30 -### Added - -- Added the annualized dividend yield to the portfolio summary (experimental) - ### Changed - Improved the usability of the create asset profile dialog in the market data section of the admin control panel diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 8ea8b51a0..000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,119 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Ghostfolio is an open source wealth management software built with TypeScript in an Nx monorepo workspace. It's a full-stack application with Angular frontend and NestJS backend, using PostgreSQL with Prisma ORM and Redis for caching. - -## Development Commands - -### Environment Setup - -```bash -npm install -docker compose -f docker/docker-compose.dev.yml up -d # Start PostgreSQL and Redis -npm run database:setup # Initialize database schema -``` - -### Development Servers - -```bash -npm run start:server # Start NestJS API server -npm run start:client # Start Angular client (English) -npm run watch:server # Start server in watch mode for debugging -``` - -### Build and Production - -```bash -npm run build:production # Build both API and client for production -npm run start:production # Run production build with database migration -``` - -### Database Operations - -```bash -npm run database:push # Sync schema with database (development) -npm run database:migrate # Apply migrations (production) -npm run database:seed # Seed database with initial data -npm run database:gui # Open Prisma Studio -npm run database:format-schema # Format Prisma schema -npm run database:generate-typings # Generate Prisma client -``` - -### Testing and Quality - -```bash -npm test # Run all tests (API + common) -npm run test:api # Run API tests only -npm run test:common # Run common library tests -npm run test:single # Run single test file (example provided) -npm run lint # Run ESLint on all projects -npm run format # Format code with Prettier -npm run format:check # Check code formatting -``` - -### Nx Workspace Commands - -```bash -nx affected:build # Build affected projects -nx affected:test # Test affected projects -nx affected:lint # Lint affected projects -nx dep-graph # View dependency graph -``` - -### Storybook (Component Library) - -```bash -npm run start:storybook # Start Storybook development server -npm run build:storybook # Build Storybook for production -``` - -## Architecture - -### Monorepo Structure - -- **apps/api**: NestJS backend application -- **apps/client**: Angular frontend application -- **apps/client-e2e**: E2E tests for client -- **apps/ui-e2e**: E2E tests for UI components -- **libs/common**: Shared TypeScript libraries and utilities -- **libs/ui**: Angular UI component library - -### Technology Stack - -- **Frontend**: Angular 20 with Angular Material, Bootstrap utility classes -- **Backend**: NestJS with TypeScript -- **Database**: PostgreSQL with Prisma ORM -- **Caching**: Redis with Bull for job queues -- **Build Tool**: Nx workspace -- **Testing**: Jest for unit tests, Cypress for E2E tests - -### Key Dependencies - -- **Authentication**: Passport (JWT, Google OAuth, WebAuthn) -- **Data Sources**: Yahoo Finance, CoinGecko APIs for market data -- **Charts**: Chart.js with various plugins -- **Payment**: Stripe integration -- **Internationalization**: Angular i18n with multiple language support - -### Database Schema - -The Prisma schema defines models for: - -- User management and access control -- Account and portfolio tracking -- Trading activities and orders -- Market data and asset information -- Platform integrations - -### Development Notes - -- Node.js version >=22.18.0 required -- Uses Nx generators for consistent code scaffolding -- Husky for git hooks and code quality enforcement -- Environment files: `.env.dev` for development, `.env.example` as template -- SSL certificates can be generated for localhost development -- Experimental features can be toggled via user settings -- always run the .husky pre commit hooks after code changes diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 67a21ab33..dc1a0d5ff 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -188,9 +188,9 @@ export abstract class PortfolioCalculator { if (!transactionPoints.length) { return { activitiesCount: 0, - dividendYieldTrailingTwelveMonths: 0, createdAt: new Date(), currentValueInBaseCurrency: new Big(0), + dividendYieldTrailingTwelveMonths: 0, errors: [], hasErrors: false, historicalData: [], @@ -439,11 +439,11 @@ export abstract class PortfolioCalculator { : 0; positions.push({ + dividendYieldTrailingTwelveMonths, includeInTotalAssetValue, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, activitiesCount: item.activitiesCount, - dividendYieldTrailingTwelveMonths, averagePrice: item.averagePrice, currency: item.currency, dataSource: item.dataSource, diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index d9cb4f808..9873771f0 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -231,13 +231,13 @@ describe('PortfolioCalculator', () => { */ expect(position).toMatchObject({ activitiesCount: 2, - dividendYieldTrailingTwelveMonths: 0, averagePrice: new Big(1), currency: 'USD', dataSource: DataSource.YAHOO, dateOfFirstActivity: '2023-12-31', dividend: new Big(0), dividendInBaseCurrency: new Big(0), + dividendYieldTrailingTwelveMonths: 0, fee: new Big(0), feeInBaseCurrency: new Big(0), grossPerformance: new Big(0), 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 e82f9ee4f..2325d2087 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 @@ -289,8 +289,7 @@ describe('PortfolioCalculator', () => { fee: new Big('19'), quantity: new Big('1'), symbol: 'MSFT', - tags: [], - transactionCount: 5 + tags: [] } ], totalFeesWithCurrencyEffect: new Big('19'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index b20200766..bc534ca16 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -125,6 +125,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { return { currentValueInBaseCurrency, + dividendYieldTrailingTwelveMonths, hasErrors, positions, totalFeesWithCurrencyEffect, @@ -134,7 +135,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { activitiesCount: this.activities.filter(({ type }) => { return ['BUY', 'SELL'].includes(type); }).length, - dividendYieldTrailingTwelveMonths, createdAt: new Date(), errors: [], historicalData: [], diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a288dc60f..96b98eaa0 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1861,8 +1861,8 @@ export class PortfolioService { } const { - dividendYieldTrailingTwelveMonths, currentValueInBaseCurrency, + dividendYieldTrailingTwelveMonths, totalInvestment } = await portfolioCalculator.getSnapshot(); @@ -1962,11 +1962,11 @@ export class PortfolioService { })?.toNumber(); return { - dividendYieldTrailingTwelveMonths, annualizedPerformancePercent, annualizedPerformancePercentWithCurrencyEffect, cash, dateOfFirstActivity, + dividendYieldTrailingTwelveMonths, excludedAccountsAndActivities, netPerformance, netPerformancePercentage, diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index 532870791..82a7ae6ae 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -3,13 +3,13 @@ import { PortfolioPerformance } from './portfolio-performance.interface'; export interface PortfolioSummary extends PortfolioPerformance { activityCount: number; - dividendYieldTrailingTwelveMonths: number; annualizedPerformancePercent: number; annualizedPerformancePercentWithCurrencyEffect: number; cash: number; committedFunds: number; dateOfFirstActivity: Date; dividendInBaseCurrency: number; + dividendYieldTrailingTwelveMonths: number; emergencyFund: { assets: number; cash: number; diff --git a/libs/common/src/lib/models/portfolio-snapshot.ts b/libs/common/src/lib/models/portfolio-snapshot.ts index 33bab356c..968d2a565 100644 --- a/libs/common/src/lib/models/portfolio-snapshot.ts +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -10,7 +10,6 @@ import { Transform, Type } from 'class-transformer'; export class PortfolioSnapshot { activitiesCount: number; - dividendYieldTrailingTwelveMonths: number; createdAt: Date; @@ -18,6 +17,8 @@ export class PortfolioSnapshot { @Type(() => Big) currentValueInBaseCurrency: Big; + dividendYieldTrailingTwelveMonths: number; + errors: AssetProfileIdentifier[]; hasErrors: boolean; diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index ba4a93482..6426c3392 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -10,7 +10,6 @@ import { Transform, Type } from 'class-transformer'; export class TimelinePosition { activitiesCount: number; - dividendYieldTrailingTwelveMonths: number; @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) @@ -28,6 +27,8 @@ export class TimelinePosition { @Type(() => Big) dividendInBaseCurrency: Big; + dividendYieldTrailingTwelveMonths: number; + @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) fee: Big; From 329c69012421a9dabe9c0eb8a1a3fc27f7873cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Mon, 9 Feb 2026 17:03:17 +0100 Subject: [PATCH 12/13] update portfolio.order-interface --- .../app/portfolio/calculator/roai/portfolio-calculator.ts | 3 +++ .../app/portfolio/interfaces/portfolio-order.interface.ts | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index bc534ca16..8532889c8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -322,6 +322,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { // Add a synthetic order at the start and the end date orders.push({ + currency: undefined, date: startDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), @@ -337,6 +338,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }); orders.push({ + currency: undefined, date: endDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), @@ -378,6 +380,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } else { orders.push({ + currency: undefined, date: dateString, fee: new Big(0), feeInBaseCurrency: new Big(0), diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 96e116e9a..5aeab7543 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -1,7 +1,9 @@ import { Activity } from '@ghostfolio/common/interfaces'; -export interface PortfolioOrder extends Pick { - currency?: string; +export interface PortfolioOrder extends Pick< + Activity, + 'currency' | 'tags' | 'type' +> { date: string; fee: Big; feeInBaseCurrency: Big; From 6b238fef0961ee0c251196bb106d37e672417ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sven=20G=C3=BCnther?= Date: Mon, 9 Feb 2026 17:03:42 +0100 Subject: [PATCH 13/13] revert locales --- apps/client/src/locales/messages.ca.xlf | 8 - apps/client/src/locales/messages.de.xlf | 8 - apps/client/src/locales/messages.es.xlf | 188 ++++++++++++------------ apps/client/src/locales/messages.fr.xlf | 8 - apps/client/src/locales/messages.it.xlf | 8 - apps/client/src/locales/messages.ko.xlf | 8 - apps/client/src/locales/messages.nl.xlf | 8 - apps/client/src/locales/messages.pl.xlf | 8 - apps/client/src/locales/messages.pt.xlf | 8 - apps/client/src/locales/messages.tr.xlf | 8 - apps/client/src/locales/messages.uk.xlf | 8 - apps/client/src/locales/messages.xlf | 7 - apps/client/src/locales/messages.zh.xlf | 8 - 13 files changed, 90 insertions(+), 193 deletions(-) diff --git a/apps/client/src/locales/messages.ca.xlf b/apps/client/src/locales/messages.ca.xlf index 45b6866a2..a6bec7df3 100644 --- a/apps/client/src/locales/messages.ca.xlf +++ b/apps/client/src/locales/messages.ca.xlf @@ -2274,14 +2274,6 @@ 216 - - Annualized Dividend Yield - Rendiment de dividends anualitzat - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Proveïdor de dades professional diff --git a/apps/client/src/locales/messages.de.xlf b/apps/client/src/locales/messages.de.xlf index b7cf14920..44ee607c3 100644 --- a/apps/client/src/locales/messages.de.xlf +++ b/apps/client/src/locales/messages.de.xlf @@ -3757,14 +3757,6 @@ 34 - - Annualized Dividend Yield - Annualisierte Dividendenrendite - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Professioneller Datenanbieter diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 6ed792d62..121e9e949 100644 --- a/apps/client/src/locales/messages.es.xlf +++ b/apps/client/src/locales/messages.es.xlf @@ -40,7 +40,7 @@ please - por favor + please apps/client/src/app/pages/pricing/pricing-page.html 333 @@ -84,7 +84,7 @@ with - con + with apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 87 @@ -368,7 +368,7 @@ and is driven by the efforts of its contributors - y es impulsado por los esfuerzos de sus contribuidores + and is driven by the efforts of its contributors apps/client/src/app/pages/about/overview/about-overview-page.html 49 @@ -652,7 +652,7 @@ No auto-renewal on membership. - No se renueva automáticamente la membresía. + No auto-renewal on membership. apps/client/src/app/components/user-account-membership/user-account-membership.html 74 @@ -1096,7 +1096,7 @@ Performance with currency effect - Rendimiento con el efecto del tipo de cambio de divisa + Performance with currency effect apps/client/src/app/pages/portfolio/analysis/analysis-page.html 135 @@ -1912,7 +1912,7 @@ Current week - Semana actual + Current week apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 191 @@ -2076,7 +2076,7 @@ or start a discussion at - o iniciar una discusión en + or start a discussion at apps/client/src/app/pages/about/overview/about-overview-page.html 94 @@ -2148,7 +2148,7 @@ Sustainable retirement income - Ingreso sostenible de retiro + Sustainable retirement income apps/client/src/app/pages/portfolio/fire/fire-page.html 41 @@ -2320,7 +2320,7 @@ contact us - contactarnos + contact us apps/client/src/app/pages/pricing/pricing-page.html 336 @@ -2420,7 +2420,7 @@ Latest activities - Últimas actividades + Latest activities apps/client/src/app/pages/public/public-page.html 211 @@ -2536,7 +2536,7 @@ annual interest rate - tasa de interés anual + annual interest rate apps/client/src/app/pages/portfolio/fire/fire-page.html 185 @@ -2656,7 +2656,7 @@ Could not validate form - No se pudo validar el formulario + Could not validate form apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 554 @@ -2892,7 +2892,7 @@ Authentication - Autenticación + Authentication apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 35 @@ -3044,7 +3044,7 @@ If you retire today, you would be able to withdraw - Si te retirases hoy, podrías sacar + If you retire today, you would be able to withdraw apps/client/src/app/pages/portfolio/fire/fire-page.html 68 @@ -3112,7 +3112,7 @@ Looking for a student discount? - ¿Buscando un descuento para estudiantes? + Looking for a student discount? apps/client/src/app/pages/pricing/pricing-page.html 342 @@ -3348,7 +3348,7 @@ Everything in Basic, plus - Todo en Básico, más + Everything in Basic, plus apps/client/src/app/pages/pricing/pricing-page.html 199 @@ -3608,7 +3608,7 @@ Could not save asset profile - No se pudo guardar el perfil del activo + Could not save asset profile apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 588 @@ -3734,14 +3734,6 @@ 32 - - Annualized Dividend Yield - Rendimiento de dividendos anualizado - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Proveedor de datos profesional @@ -3820,7 +3812,7 @@ By - Por + By apps/client/src/app/pages/portfolio/fire/fire-page.html 139 @@ -3836,7 +3828,7 @@ Current year - Año actual + Current year apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 199 @@ -3872,7 +3864,7 @@ Asset profile has been saved - El perfil del activo ha sido guardado + Asset profile has been saved apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 578 @@ -4064,7 +4056,7 @@ View Details - Ver detalles + View Details apps/client/src/app/components/admin-users/admin-users.html 225 @@ -4200,7 +4192,7 @@ per week - por semana + per week apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 130 @@ -4224,7 +4216,7 @@ and we share aggregated key metrics of the platform’s performance - y compartimos agregados métricas clave del rendimiento de la plataforma + and we share aggregated key metrics of the platform’s performance apps/client/src/app/pages/about/overview/about-overview-page.html 32 @@ -4268,7 +4260,7 @@ Website of Thomas Kaul - Sitio web de Thomas Kaul + Website of Thomas Kaul apps/client/src/app/pages/about/overview/about-overview-page.html 44 @@ -4448,7 +4440,7 @@ Sign in with OpenID Connect - Iniciar sesión con OpenID Connect + Sign in with OpenID Connect apps/client/src/app/components/login-with-access-token-dialog/login-with-access-token-dialog.html 55 @@ -4540,7 +4532,7 @@ The source code is fully available as open source software (OSS) under the AGPL-3.0 license - El código fuente está disponible completamente en software de código abierto (OSS) bajo la licencia AGPL-3.0 + The source code is fully available as open source software (OSS) under the AGPL-3.0 license apps/client/src/app/pages/about/overview/about-overview-page.html 16 @@ -4612,7 +4604,7 @@ this is projected to increase to - esto se proyecta a aumentar a + this is projected to increase to apps/client/src/app/pages/portfolio/fire/fire-page.html 147 @@ -4664,7 +4656,7 @@ Job ID - ID de trabajo + Job ID apps/client/src/app/components/admin-jobs/admin-jobs.html 34 @@ -4748,7 +4740,7 @@ for - para + for apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 128 @@ -4772,7 +4764,7 @@ Could not parse scraper configuration - No se pudo analizar la configuración del scraper + Could not parse scraper configuration apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 509 @@ -4816,7 +4808,7 @@ Edit access - Editar acceso + Edit access apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.html 11 @@ -4888,7 +4880,7 @@ Get access to 80’000+ tickers from over 50 exchanges - Obtén acceso a más de 80,000 tickers de más de 50 exchanges + Get access to 80’000+ tickers from over 50 exchanges apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 84 @@ -5072,7 +5064,7 @@ less than - menos que + less than apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 129 @@ -5362,7 +5354,7 @@ Ghostfolio Status - Estado de Ghostfolio + Ghostfolio Status apps/client/src/app/pages/about/overview/about-overview-page.html 62 @@ -5370,7 +5362,7 @@ with your university e-mail address - con tu dirección de correo electrónico de la universidad + with your university e-mail address apps/client/src/app/pages/pricing/pricing-page.html 348 @@ -5390,7 +5382,7 @@ and a safe withdrawal rate (SWR) of - y una tasa de retiro segura (SWR) de + and a safe withdrawal rate (SWR) of apps/client/src/app/pages/portfolio/fire/fire-page.html 108 @@ -5554,7 +5546,7 @@ Request it - Solicitar + Request it apps/client/src/app/pages/pricing/pricing-page.html 344 @@ -5610,7 +5602,7 @@ , - , + , apps/client/src/app/pages/portfolio/fire/fire-page.html 145 @@ -5626,7 +5618,7 @@ per month - por mes + per month apps/client/src/app/pages/portfolio/fire/fire-page.html 94 @@ -5874,7 +5866,7 @@ here - aquí + here apps/client/src/app/pages/pricing/pricing-page.html 347 @@ -5882,7 +5874,7 @@ Close Holding - Cerrar posición + Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 442 @@ -6183,7 +6175,7 @@ {VAR_PLURAL, plural, =1 {activity} other {activities}} - {VAR_PLURAL, plural, =1 {actividad} other {actividades}} + {VAR_PLURAL, plural, =1 {activity} other {activities}} apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html 14 @@ -6263,7 +6255,7 @@ Include in - Incluir en + Include in apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 374 @@ -6547,7 +6539,7 @@ View Holding - Ver fondos + View Holding libs/ui/src/lib/activities-table/activities-table.component.html 450 @@ -6691,7 +6683,7 @@ Oops! Could not update access. - Oops! No se pudo actualizar el acceso. + Oops! Could not update access. apps/client/src/app/components/user-account-access/create-or-update-access-dialog/create-or-update-access-dialog.component.ts 178 @@ -6699,7 +6691,7 @@ , based on your total assets of - , basado en tus activos totales de + , based on your total assets of apps/client/src/app/pages/portfolio/fire/fire-page.html 96 @@ -6771,7 +6763,7 @@ Close - Cerrar + Cerca apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 594 @@ -6815,7 +6807,7 @@ Role - Rol + Role apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 14 @@ -6847,7 +6839,7 @@ Change with currency effect Change - Cambiar con efecto de cambio dedivisa Cambiar + Change with currency effect Change apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 63 @@ -6855,7 +6847,7 @@ If you plan to open an account at - Si planeas abrir una cuenta en + If you plan to open an account at apps/client/src/app/pages/pricing/pricing-page.html 312 @@ -6863,7 +6855,7 @@ Performance with currency effect Performance - Rendimiento con cambio de divisa Rendimiento + Performance with currency effect Performance apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 83 @@ -6887,7 +6879,7 @@ send an e-mail to - enviar un correo electrónico a + send an e-mail to apps/client/src/app/pages/about/overview/about-overview-page.html 87 @@ -6959,7 +6951,7 @@ , assuming a - , asumiendo un + , assuming a apps/client/src/app/pages/portfolio/fire/fire-page.html 174 @@ -6967,7 +6959,7 @@ to use our referral link and get a Ghostfolio Premium membership for one year - para usar nuestro enlace de referido y obtener una membresía Ghostfolio Premium por un año + to use our referral link and get a Ghostfolio Premium membership for one year apps/client/src/app/pages/pricing/pricing-page.html 340 @@ -7047,7 +7039,7 @@ Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. - Ghostfolio es una aplicación de gestión de patrimonio para aquellos individuos que desean realizar un seguimiento de acciones, ETFs o criptomonedas y tomar decisiones de inversión sólidas y basadas en datos. + Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions. apps/client/src/app/pages/about/overview/about-overview-page.html 10 @@ -7361,7 +7353,7 @@ Check the system status at - Verificar el estado del sistema en + Check the system status at apps/client/src/app/pages/about/overview/about-overview-page.html 57 @@ -7377,7 +7369,7 @@ Change with currency effect - Cambiar con el efecto del tipo de cambio de divisa + Change with currency effect apps/client/src/app/pages/portfolio/analysis/analysis-page.html 116 @@ -7517,7 +7509,7 @@ The project has been initiated by - El proyecto ha sido iniciado por + The project has been initiated by apps/client/src/app/pages/about/overview/about-overview-page.html 40 @@ -7541,7 +7533,7 @@ Total amount - Cantidad total + Total amount apps/client/src/app/pages/portfolio/analysis/analysis-page.html 95 @@ -7633,7 +7625,7 @@ Find account, holding or page... - Buscar cuenta, posición o página... + Find account, holding or page... libs/ui/src/lib/assistant/assistant.component.ts 151 @@ -8057,7 +8049,7 @@ Current month - Mes actual + Current month apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts 195 @@ -8242,7 +8234,7 @@ If you encounter a bug, would like to suggest an improvement or a new feature, please join the Ghostfolio Slack community, post to @ghostfolio_ - Si encuentras un error, deseas sugerir una mejora o una nueva característica, por favor únete a la comunidad Ghostfolio Slack, publica en @ghostfolio_ + If you encounter a bug, would like to suggest an improvement or a new feature, please join the Ghostfolio Slack community, post to @ghostfolio_ apps/client/src/app/pages/about/overview/about-overview-page.html 69 @@ -8274,7 +8266,7 @@ - + apps/client/src/app/components/admin-users/admin-users.html 39 @@ -8342,7 +8334,7 @@ Economic Market Cluster Risks - Riesgos del clúster de mercados económicos + Economic Market Cluster Risks apps/client/src/app/pages/i18n/i18n-page.html 106 @@ -8350,7 +8342,7 @@ Emergency Fund - Fondo de emergencia + Emergency Fund apps/client/src/app/pages/i18n/i18n-page.html 144 @@ -8358,7 +8350,7 @@ Fees - Comisiones + Fees apps/client/src/app/pages/i18n/i18n-page.html 161 @@ -8366,7 +8358,7 @@ Liquidity - Liquidez + Liquidity apps/client/src/app/pages/i18n/i18n-page.html 70 @@ -8374,7 +8366,7 @@ Buying Power - Poder de compra + Buying Power apps/client/src/app/pages/i18n/i18n-page.html 71 @@ -8382,7 +8374,7 @@ Your buying power is below ${thresholdMin} ${baseCurrency} - Tu poder de compra es inferior a ${thresholdMin} ${baseCurrency} + Your buying power is below ${thresholdMin} ${baseCurrency} apps/client/src/app/pages/i18n/i18n-page.html 73 @@ -8390,7 +8382,7 @@ Your buying power is 0 ${baseCurrency} - Tu poder de compra es 0 ${baseCurrency} + Your buying power is 0 ${baseCurrency} apps/client/src/app/pages/i18n/i18n-page.html 77 @@ -8398,7 +8390,7 @@ Your buying power exceeds ${thresholdMin} ${baseCurrency} - Tu poder de compra excede ${thresholdMin} ${baseCurrency} + Your buying power exceeds ${thresholdMin} ${baseCurrency} apps/client/src/app/pages/i18n/i18n-page.html 80 @@ -8430,7 +8422,7 @@ The developed markets contribution of your current investment (${developedMarketsValueRatio}%) exceeds ${thresholdMax}% - La contribución a los mercados desarrollados de tu inversión actual (${developedMarketsValueRatio}%) supera el ${thresholdMax}% + The developed markets contribution of your current investment (${developedMarketsValueRatio}%) exceeds ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 112 @@ -8438,7 +8430,7 @@ The developed markets contribution of your current investment (${developedMarketsValueRatio}%) is below ${thresholdMin}% - La contribución a los mercados desarrollados de tu inversión actual (${developedMarketsValueRatio}%) es inferior al ${thresholdMin}% + The developed markets contribution of your current investment (${developedMarketsValueRatio}%) is below ${thresholdMin}% apps/client/src/app/pages/i18n/i18n-page.html 117 @@ -8446,7 +8438,7 @@ The developed markets contribution of your current investment (${developedMarketsValueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% - La contribución a los mercados desarrollados de tu inversión actual (${developedMarketsValueRatio}%) está dentro del rango de ${thresholdMin}% y ${thresholdMax}% + The developed markets contribution of your current investment (${developedMarketsValueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 122 @@ -8462,7 +8454,7 @@ The emerging markets contribution of your current investment (${emergingMarketsValueRatio}%) exceeds ${thresholdMax}% - La contribución a los mercados emergentes de tu inversión actual (${emergingMarketsValueRatio}%) supera el ${thresholdMax}% + The emerging markets contribution of your current investment (${emergingMarketsValueRatio}%) exceeds ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 130 @@ -8470,7 +8462,7 @@ The emerging markets contribution of your current investment (${emergingMarketsValueRatio}%) is below ${thresholdMin}% - La contribución a los mercados emergentes de tu inversión actual (${emergingMarketsValueRatio}%) es inferior al ${thresholdMin}% + The emerging markets contribution of your current investment (${emergingMarketsValueRatio}%) is below ${thresholdMin}% apps/client/src/app/pages/i18n/i18n-page.html 135 @@ -8478,7 +8470,7 @@ The emerging markets contribution of your current investment (${emergingMarketsValueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% - La contribución a los mercados emergentes de tu inversión actual (${emergingMarketsValueRatio}%) está dentro del rango de ${thresholdMin}% y ${thresholdMax}% + The emerging markets contribution of your current investment (${emergingMarketsValueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 140 @@ -8502,7 +8494,7 @@ Asia-Pacific - Asia-Pacífico + Asia-Pacific apps/client/src/app/pages/i18n/i18n-page.html 165 @@ -8510,7 +8502,7 @@ The Asia-Pacific market contribution of your current investment (${valueRatio}%) exceeds ${thresholdMax}% - La contribución al mercado de Asia-Pacífico de tu inversión actual (${valueRatio}%) supera el ${thresholdMax}% + The Asia-Pacific market contribution of your current investment (${valueRatio}%) exceeds ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 167 @@ -8518,7 +8510,7 @@ The Asia-Pacific market contribution of your current investment (${valueRatio}%) is below ${thresholdMin}% - La contribución al mercado de Asia-Pacífico de tu inversión actual (${valueRatio}%) es inferior al ${thresholdMin}% + The Asia-Pacific market contribution of your current investment (${valueRatio}%) is below ${thresholdMin}% apps/client/src/app/pages/i18n/i18n-page.html 171 @@ -8526,7 +8518,7 @@ The Asia-Pacific market contribution of your current investment (${valueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% - La contribución al mercado de Asia-Pacífico de tu inversión actual (${valueRatio}%) está dentro del rango de ${thresholdMin}% y ${thresholdMax}% + The Asia-Pacific market contribution of your current investment (${valueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 175 @@ -8534,7 +8526,7 @@ Emerging Markets - Mercados emergentes + Emerging Markets apps/client/src/app/pages/i18n/i18n-page.html 180 @@ -8542,7 +8534,7 @@ The Emerging Markets contribution of your current investment (${valueRatio}%) exceeds ${thresholdMax}% - La contribución a los mercados emergentes de tu inversión actual (${valueRatio}%) supera el ${thresholdMax}% + The Emerging Markets contribution of your current investment (${valueRatio}%) exceeds ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 183 @@ -8550,7 +8542,7 @@ The Emerging Markets contribution of your current investment (${valueRatio}%) is below ${thresholdMin}% - La contribución a los mercados emergentes de tu inversión actual (${valueRatio}%) es inferior al ${thresholdMin}% + The Emerging Markets contribution of your current investment (${valueRatio}%) is below ${thresholdMin}% apps/client/src/app/pages/i18n/i18n-page.html 187 @@ -8558,7 +8550,7 @@ The Emerging Markets contribution of your current investment (${valueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% - La contribución a los mercados emergentes de tu inversión actual (${valueRatio}%) está dentro del rango de ${thresholdMin}% y ${thresholdMax}% + The Emerging Markets contribution of your current investment (${valueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 191 @@ -8566,7 +8558,7 @@ Europe - Europa + Europe apps/client/src/app/pages/i18n/i18n-page.html 195 @@ -8574,7 +8566,7 @@ The Europe market contribution of your current investment (${valueRatio}%) exceeds ${thresholdMax}% - La contribución al mercado europeo de tu inversión actual (${valueRatio}%) supera el ${thresholdMax}% + The Europe market contribution of your current investment (${valueRatio}%) exceeds ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 197 @@ -8582,7 +8574,7 @@ The Europe market contribution of your current investment (${valueRatio}%) is below ${thresholdMin}% - La contribución al mercado europeo de tu inversión actual (${valueRatio}%) es inferior al ${thresholdMin}% + The Europe market contribution of your current investment (${valueRatio}%) is below ${thresholdMin}% apps/client/src/app/pages/i18n/i18n-page.html 201 @@ -8590,7 +8582,7 @@ The Europe market contribution of your current investment (${valueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% - La contribución al mercado europeo de tu inversión actual (${valueRatio}%) está dentro del rango de ${thresholdMin}% y ${thresholdMax}% + The Europe market contribution of your current investment (${valueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% apps/client/src/app/pages/i18n/i18n-page.html 205 @@ -8702,7 +8694,7 @@ Registration Date - Fecha de registro + Registration Date apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 26 diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index d97abbcac..0da1c3f6a 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -3733,14 +3733,6 @@ 32 - - Annualized Dividend Yield - Rendement de dividende annualisé - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Fournisseur de Données Professionnel diff --git a/apps/client/src/locales/messages.it.xlf b/apps/client/src/locales/messages.it.xlf index f78e2a3df..746e1fbd1 100644 --- a/apps/client/src/locales/messages.it.xlf +++ b/apps/client/src/locales/messages.it.xlf @@ -3734,14 +3734,6 @@ 32 - - Annualized Dividend Yield - Rendimento da dividendi annualizzato - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Fornitore di dati professionale diff --git a/apps/client/src/locales/messages.ko.xlf b/apps/client/src/locales/messages.ko.xlf index fe0d92fd5..67443706b 100644 --- a/apps/client/src/locales/messages.ko.xlf +++ b/apps/client/src/locales/messages.ko.xlf @@ -2035,14 +2035,6 @@ 216 - - Annualized Dividend Yield - 연간 배당 수익률 - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider 전문 데이터 제공자 diff --git a/apps/client/src/locales/messages.nl.xlf b/apps/client/src/locales/messages.nl.xlf index f865c4707..d7d0b71e8 100644 --- a/apps/client/src/locales/messages.nl.xlf +++ b/apps/client/src/locales/messages.nl.xlf @@ -3733,14 +3733,6 @@ 32 - - Annualized Dividend Yield - Geannualiseerd dividendrendement - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Professionele gegevensleverancier diff --git a/apps/client/src/locales/messages.pl.xlf b/apps/client/src/locales/messages.pl.xlf index bf8992687..a94a76d2f 100644 --- a/apps/client/src/locales/messages.pl.xlf +++ b/apps/client/src/locales/messages.pl.xlf @@ -2002,14 +2002,6 @@ 216 - - Annualized Dividend Yield - Roczna stopa dywidendy - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Profesjonalne źródło danych diff --git a/apps/client/src/locales/messages.pt.xlf b/apps/client/src/locales/messages.pt.xlf index 788a2aa7e..2bcd7c401 100644 --- a/apps/client/src/locales/messages.pt.xlf +++ b/apps/client/src/locales/messages.pt.xlf @@ -3733,14 +3733,6 @@ 32 - - Annualized Dividend Yield - Rendimento de dividendos anualizado - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Provedor de Dados Profissionais diff --git a/apps/client/src/locales/messages.tr.xlf b/apps/client/src/locales/messages.tr.xlf index cd515c861..421ef3855 100644 --- a/apps/client/src/locales/messages.tr.xlf +++ b/apps/client/src/locales/messages.tr.xlf @@ -1870,14 +1870,6 @@ 216 - - Annualized Dividend Yield - Yıllık Temettü Getirisi - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Profesyonel Veri Sağlayıcı diff --git a/apps/client/src/locales/messages.uk.xlf b/apps/client/src/locales/messages.uk.xlf index de461f76a..2fc389030 100644 --- a/apps/client/src/locales/messages.uk.xlf +++ b/apps/client/src/locales/messages.uk.xlf @@ -2478,14 +2478,6 @@ 216 - - Annualized Dividend Yield - Річна дивідендна дохідність - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider Професійний постачальник даних diff --git a/apps/client/src/locales/messages.xlf b/apps/client/src/locales/messages.xlf index 64c91b108..a6907698d 100644 --- a/apps/client/src/locales/messages.xlf +++ b/apps/client/src/locales/messages.xlf @@ -1867,13 +1867,6 @@ 216 - - Annualized Dividend Yield - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider diff --git a/apps/client/src/locales/messages.zh.xlf b/apps/client/src/locales/messages.zh.xlf index cb60c8550..7ec9d85a0 100644 --- a/apps/client/src/locales/messages.zh.xlf +++ b/apps/client/src/locales/messages.zh.xlf @@ -2011,14 +2011,6 @@ 216 - - Annualized Dividend Yield - 年化股息收益率 - - apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html - 379 - - Professional Data Provider 专业数据提供商