diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ddad0b5..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 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 2e58a4ef5..dc1a0d5ff 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -54,7 +54,8 @@ import { isWithinInterval, min, startOfYear, - subDays + subDays, + subYears } from 'date-fns'; import { isNumber, sortBy, sum, uniqBy } from 'lodash'; @@ -118,6 +119,7 @@ export abstract class PortfolioCalculator { this.activities = activities .map( ({ + currency, date, feeInAssetProfileCurrency, feeInBaseCurrency, @@ -138,6 +140,7 @@ export abstract class PortfolioCalculator { } return { + currency, SymbolProfile, tags, type, @@ -187,6 +190,7 @@ export abstract class PortfolioCalculator { activitiesCount: 0, createdAt: new Date(), currentValueInBaseCurrency: new Big(0), + dividendYieldTrailingTwelveMonths: 0, errors: [], hasErrors: false, historicalData: [], @@ -403,7 +407,39 @@ export abstract class PortfolioCalculator { }; } + // 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' && + isWithinInterval(new Date(date), { + start: twelveMonthsAgo, + end: this.endDate + }) + ); + }) + .reduce((sum, activity) => { + const activityCurrency = + activity.currency ?? activity.SymbolProfile.currency; + const exchangeRate = + 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 dividendYieldTrailingTwelveMonths = + totalInvestmentWithCurrencyEffect.gt(0) + ? dividendsLast12Months + .div(totalInvestmentWithCurrencyEffect) + .toNumber() + : 0; + positions.push({ + dividendYieldTrailingTwelveMonths, includeInTotalAssetValue, timeWeightedInvestment, timeWeightedInvestmentWithCurrencyEffect, 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..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 @@ -237,6 +237,7 @@ describe('PortfolioCalculator', () => { 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-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 b19adb642..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 @@ -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,155 @@ 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'), + quantity: new Big('1'), + symbol: 'MSFT', + tags: [] + } + ], + 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); + + const expectedPositionYield = new Big(position.dividendInBaseCurrency) + .div(position.investmentWithCurrencyEffect) + .toNumber(); + expect(position.dividendYieldTrailingTwelveMonths).toBeCloseTo( + expectedPositionYield, + 10 + ); + expect(expectedPositionYield).toBeCloseTo(0.00891, 3); // ~0.89% yield on cost + + // Verify portfolio-level dividend yield + expect(portfolioSnapshot).toHaveProperty( + 'dividendYieldTrailingTwelveMonths' + ); + expect(portfolioSnapshot.dividendYieldTrailingTwelveMonths).toBeCloseTo( + expectedPositionYield, + 10 + ); + + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( + expect.objectContaining({ + totalInvestmentValueWithCurrencyEffect: 298.58 + }) + ); + }); }); }); 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..8532889c8 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 totalDividendsTrailingTwelveMonthsInBaseCurrency = new Big(0); let totalFeesWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); @@ -46,6 +47,15 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { return includeInTotalAssetValue; } )) { + if (currentPosition.investmentWithCurrencyEffect) { + totalDividendsTrailingTwelveMonthsInBaseCurrency = + totalDividendsTrailingTwelveMonthsInBaseCurrency.plus( + new Big(currentPosition.dividendYieldTrailingTwelveMonths ?? 0).mul( + currentPosition.investmentWithCurrencyEffect + ) + ); + } + if (currentPosition.feeInBaseCurrency) { totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( currentPosition.feeInBaseCurrency @@ -105,8 +115,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } + // Calculate dividend yield for the entire portfolio based on trailing twelve months + const dividendYieldTrailingTwelveMonths = + totalInvestmentWithCurrencyEffect.gt(0) + ? totalDividendsTrailingTwelveMonthsInBaseCurrency + .div(totalInvestmentWithCurrencyEffect) + .toNumber() + : 0; + return { currentValueInBaseCurrency, + dividendYieldTrailingTwelveMonths, hasErrors, positions, totalFeesWithCurrencyEffect, @@ -303,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), @@ -318,6 +338,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }); orders.push({ + currency: undefined, date: endDateString, fee: new Big(0), feeInBaseCurrency: new Big(0), @@ -359,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/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/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 2dbd68f12..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,6 +1,9 @@ import { Activity } from '@ghostfolio/common/interfaces'; -export interface PortfolioOrder extends Pick { +export interface PortfolioOrder extends Pick< + Activity, + 'currency' | 'tags' | 'type' +> { 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 new file mode 100644 index 000000000..3c7a293c5 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -0,0 +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', () => { + beforeEach(() => { + jest.useFakeTimers().setSystemTime(new Date('2023-07-10')); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); + }); + + it('returns dividendYieldTrailingTwelveMonths 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 orderService = { + getOrders: jest.fn().mockResolvedValue({ activities }) + }; + + const userService = { + user: jest.fn().mockResolvedValue({ + id: userDummyData.id, + settings: { + settings: { + baseCurrency: DEFAULT_CURRENCY, + emergencyFund: 0 + } + } + }) + }; + + const accountService = { + getCashDetails: jest.fn().mockResolvedValue({ + balanceInBaseCurrency: 1000 + }) + }; + + const impersonationService = { + validateImpersonationId: jest.fn().mockResolvedValue(undefined) + }; + + 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({ + dividendYieldTrailingTwelveMonths: 0.0123, + currentValueInBaseCurrency: new Big(500), + totalInvestment: new Big(400) + }), + getStartDate: jest.fn().mockReturnValue(new Date('2023-01-01')) + } as unknown as PortfolioCalculator; + + 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 + }); + + expect(portfolioCalculator.getSnapshot).toHaveBeenCalledTimes(1); + expect(summary).toMatchObject({ + dividendYieldTrailingTwelveMonths: 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')); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7be375473..96b98eaa0 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 { + currentValueInBaseCurrency, + dividendYieldTrailingTwelveMonths, + totalInvestment + } = await portfolioCalculator.getSnapshot(); const { performance } = await this.getPerformance({ impersonationId, @@ -1963,6 +1966,7 @@ export class PortfolioService { annualizedPerformancePercentWithCurrencyEffect, cash, dateOfFirstActivity, + dividendYieldTrailingTwelveMonths, excludedAccountsAndActivities, netPerformance, netPerformancePercentage, diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 46eb2845c..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 @@ -374,4 +374,25 @@ /> + @if (user?.settings?.isExperimentalFeatures) { +
+
+ Dividend Yield (Trailing Twelve Months) +
+
+ +
+
+ } diff --git a/apps/client/src/locales/messages.es.xlf b/apps/client/src/locales/messages.es.xlf index 7564e4d80..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 @@ -3812,7 +3812,7 @@ By - Por + By apps/client/src/app/pages/portfolio/fire/fire-page.html 139 @@ -3828,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 @@ -3864,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 @@ -4056,7 +4056,7 @@ View Details - Ver detalles + View Details apps/client/src/app/components/admin-users/admin-users.html 225 @@ -4192,7 +4192,7 @@ per week - por semana + per week apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 130 @@ -4216,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 @@ -4260,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 @@ -4440,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 @@ -4532,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 @@ -4604,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 @@ -4656,7 +4656,7 @@ Job ID - ID de trabajo + Job ID apps/client/src/app/components/admin-jobs/admin-jobs.html 34 @@ -4740,7 +4740,7 @@ for - para + for apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 128 @@ -4764,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 @@ -4808,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 @@ -4880,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 @@ -5064,7 +5064,7 @@ less than - menos que + less than apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html 129 @@ -5354,7 +5354,7 @@ Ghostfolio Status - Estado de Ghostfolio + Ghostfolio Status apps/client/src/app/pages/about/overview/about-overview-page.html 62 @@ -5362,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 @@ -5382,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 @@ -5546,7 +5546,7 @@ Request it - Solicitar + Request it apps/client/src/app/pages/pricing/pricing-page.html 344 @@ -5602,7 +5602,7 @@ , - , + , apps/client/src/app/pages/portfolio/fire/fire-page.html 145 @@ -5618,7 +5618,7 @@ per month - por mes + per month apps/client/src/app/pages/portfolio/fire/fire-page.html 94 @@ -5866,7 +5866,7 @@ here - aquí + here apps/client/src/app/pages/pricing/pricing-page.html 347 @@ -5874,7 +5874,7 @@ Close Holding - Cerrar posición + Close Holding apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html 442 @@ -6175,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 @@ -6255,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 @@ -6539,7 +6539,7 @@ View Holding - Ver fondos + View Holding libs/ui/src/lib/activities-table/activities-table.component.html 450 @@ -6683,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 @@ -6691,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 @@ -6763,7 +6763,7 @@ Close - Cerrar + Cerca apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html 594 @@ -6807,7 +6807,7 @@ Role - Rol + Role apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html 14 @@ -6839,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 @@ -6847,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 @@ -6855,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 @@ -6879,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 @@ -6951,7 +6951,7 @@ , assuming a - , asumiendo un + , assuming a apps/client/src/app/pages/portfolio/fire/fire-page.html 174 @@ -6959,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 @@ -7039,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 @@ -7353,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 @@ -7369,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 @@ -7509,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 @@ -7533,7 +7533,7 @@ Total amount - Cantidad total + Total amount apps/client/src/app/pages/portfolio/analysis/analysis-page.html 95 @@ -7625,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 @@ -8049,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 @@ -8234,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 @@ -8266,7 +8266,7 @@ - + apps/client/src/app/components/admin-users/admin-users.html 39 @@ -8334,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 @@ -8342,7 +8342,7 @@ Emergency Fund - Fondo de emergencia + Emergency Fund apps/client/src/app/pages/i18n/i18n-page.html 144 @@ -8350,7 +8350,7 @@ Fees - Comisiones + Fees apps/client/src/app/pages/i18n/i18n-page.html 161 @@ -8358,7 +8358,7 @@ Liquidity - Liquidez + Liquidity apps/client/src/app/pages/i18n/i18n-page.html 70 @@ -8366,7 +8366,7 @@ Buying Power - Poder de compra + Buying Power apps/client/src/app/pages/i18n/i18n-page.html 71 @@ -8374,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 @@ -8382,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 @@ -8390,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 @@ -8422,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 @@ -8430,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 @@ -8438,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 @@ -8454,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 @@ -8462,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 @@ -8470,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 @@ -8494,7 +8494,7 @@ Asia-Pacific - Asia-Pacífico + Asia-Pacific apps/client/src/app/pages/i18n/i18n-page.html 165 @@ -8502,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 @@ -8510,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 @@ -8518,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 @@ -8526,7 +8526,7 @@ Emerging Markets - Mercados emergentes + Emerging Markets apps/client/src/app/pages/i18n/i18n-page.html 180 @@ -8534,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 @@ -8542,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 @@ -8550,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 @@ -8558,7 +8558,7 @@ Europe - Europa + Europe apps/client/src/app/pages/i18n/i18n-page.html 195 @@ -8566,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 @@ -8574,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 @@ -8582,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 @@ -8694,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/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index ccf94dcf7..82a7ae6ae 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -9,6 +9,7 @@ export interface PortfolioSummary extends PortfolioPerformance { 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 6b13ca048..968d2a565 100644 --- a/libs/common/src/lib/models/portfolio-snapshot.ts +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -17,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 13f9001d5..6426c3392 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -27,6 +27,8 @@ export class TimelinePosition { @Type(() => Big) dividendInBaseCurrency: Big; + dividendYieldTrailingTwelveMonths: number; + @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) fee: Big;