Browse Source

Merge a7b4a9ea09 into 2520d0d961

pull/6258/merge
Sven Günther 8 hours ago
committed by GitHub
parent
commit
a65fad85db
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 29
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  4. 511
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  5. 18
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  6. 9
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  7. 176
      apps/api/src/app/portfolio/portfolio.service.spec.ts
  8. 8
      apps/api/src/app/portfolio/portfolio.service.ts
  9. 17
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  10. 8
      apps/client/src/locales/messages.ca.xlf
  11. 8
      apps/client/src/locales/messages.de.xlf
  12. 8
      apps/client/src/locales/messages.es.xlf
  13. 8
      apps/client/src/locales/messages.fr.xlf
  14. 8
      apps/client/src/locales/messages.it.xlf
  15. 8
      apps/client/src/locales/messages.ko.xlf
  16. 8
      apps/client/src/locales/messages.nl.xlf
  17. 8
      apps/client/src/locales/messages.pl.xlf
  18. 8
      apps/client/src/locales/messages.pt.xlf
  19. 8
      apps/client/src/locales/messages.tr.xlf
  20. 8
      apps/client/src/locales/messages.uk.xlf
  21. 7
      apps/client/src/locales/messages.xlf
  22. 8
      apps/client/src/locales/messages.zh.xlf
  23. 1
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  24. 1
      libs/common/src/lib/models/portfolio-snapshot.ts
  25. 1
      libs/common/src/lib/models/timeline-position.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added the annualized dividend yield to the portfolio summary (experimental)
- Added the ability to fetch top holdings for ETF and mutual fund assets from _Yahoo Finance_
### Changed

29
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,

1
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -231,6 +231,7 @@ describe('PortfolioCalculator', () => {
*/
expect(position).toMatchObject<TimelinePosition>({
activitiesCount: 2,
annualizedDividendYield: 0,
averagePrice: new Big(1),
currency: 'USD',
dataSource: DataSource.YAHOO,

511
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[] = [
@ -179,5 +179,514 @@ 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);
});
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
);
});
});
});

18
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 totalDividendsLast12MonthsInBaseCurrency = 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) {
totalDividendsLast12MonthsInBaseCurrency =
totalDividendsLast12MonthsInBaseCurrency.plus(
new Big(currentPosition.annualizedDividendYield ?? 0).mul(
currentPosition.investmentWithCurrencyEffect
)
);
}
if (currentPosition.feeInBaseCurrency) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.feeInBaseCurrency
@ -105,6 +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;
return {
currentValueInBaseCurrency,
hasErrors,
@ -116,6 +133,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
activitiesCount: this.activities.filter(({ type }) => {
return ['BUY', 'SELL'].includes(type);
}).length,
annualizedDividendYield,
createdAt: new Date(),
errors: [],
historicalData: [],

9
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 };

176
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 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 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({
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 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({
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'));
});
});
});

8
apps/api/src/app/portfolio/portfolio.service.ts

@ -1868,8 +1868,11 @@ export class PortfolioService {
}
}
const { currentValueInBaseCurrency, totalInvestment } =
await portfolioCalculator.getSnapshot();
const {
annualizedDividendYield,
currentValueInBaseCurrency,
totalInvestment
} = await portfolioCalculator.getSnapshot();
const { performance } = await this.getPerformance({
impersonationId,
@ -1967,6 +1970,7 @@ export class PortfolioService {
})?.toNumber();
return {
annualizedDividendYield,
annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect,
cash,

17
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -374,4 +374,21 @@
/>
</div>
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div class="flex-nowrap px-3 py-1 row">
<div class="flex-grow-1 ml-3 text-truncate" i18n>
Annualized Dividend Yield
</div>
<div class="flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : summary?.annualizedDividendYield"
/>
</div>
</div>
}
</div>

8
apps/client/src/locales/messages.ca.xlf

@ -2274,6 +2274,14 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Rendiment de dividends anualitzat</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Proveïdor de dades professional</target>

8
apps/client/src/locales/messages.de.xlf

@ -3757,6 +3757,14 @@
<context context-type="linenumber">34</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Annualisierte Dividendenrendite</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Professioneller Datenanbieter</target>

8
apps/client/src/locales/messages.es.xlf

@ -3734,6 +3734,14 @@
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Rendimiento de dividendos anualizado</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translate">Proveedor de datos profesional</target>

8
apps/client/src/locales/messages.fr.xlf

@ -3733,6 +3733,14 @@
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Rendement de dividende annualisé</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Fournisseur de Données Professionnel</target>

8
apps/client/src/locales/messages.it.xlf

@ -3734,6 +3734,14 @@
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Rendimento da dividendi annualizzato</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Fornitore di dati professionale</target>

8
apps/client/src/locales/messages.ko.xlf

@ -2035,6 +2035,14 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">연간 배당 수익률</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">전문 데이터 제공자</target>

8
apps/client/src/locales/messages.nl.xlf

@ -3733,6 +3733,14 @@
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Geannualiseerd dividendrendement</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Professionele gegevensleverancier</target>

8
apps/client/src/locales/messages.pl.xlf

@ -2002,6 +2002,14 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Roczna stopa dywidendy</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Profesjonalne źródło danych</target>

8
apps/client/src/locales/messages.pt.xlf

@ -3733,6 +3733,14 @@
<context context-type="linenumber">32</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Rendimento de dividendos anualizado</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Provedor de Dados Profissionais</target>

8
apps/client/src/locales/messages.tr.xlf

@ -1870,6 +1870,14 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Yıllık Temettü Getirisi</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Profesyonel Veri Sağlayıcı</target>

8
apps/client/src/locales/messages.uk.xlf

@ -2478,6 +2478,14 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">Річна дивідендна дохідність</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">Професійний постачальник даних</target>

7
apps/client/src/locales/messages.xlf

@ -1867,6 +1867,13 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<context-group purpose="location">

8
apps/client/src/locales/messages.zh.xlf

@ -2011,6 +2011,14 @@
<context context-type="linenumber">216</context>
</context-group>
</trans-unit>
<trans-unit id="2480205436092170223" datatype="html">
<source>Annualized Dividend Yield</source>
<target state="translated">年化股息收益率</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">379</context>
</context-group>
</trans-unit>
<trans-unit id="2486744036183712016" datatype="html">
<source>Professional Data Provider</source>
<target state="translated">专业数据提供商</target>

1
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;

1
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;

1
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)

Loading…
Cancel
Save