Browse Source

Add specs of ROI portfolio calculator for existing scenarios

pull/8188/head
Pedro Moreira 1 week ago
parent
commit
381de12a45
  1. 219
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-export-files.spec.ts
  2. 177
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-googl-buy-with-dividend.spec.ts

219
apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-export-files.spec.ts

@ -0,0 +1,219 @@
import {
loadActivitiesFromExportFile,
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 { 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('RoiPortfolioCalculator', () => {
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
beforeEach(() => {
PortfolioSnapshotServiceMock.reset();
RedisCacheServiceMock.reset();
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
new ConfigurationService(),
new CurrentRateService(null, null, null),
new ExchangeRateDataService(null, null, null, null),
new PortfolioSnapshotService(null, null),
new RedisCacheService(null, null)
);
});
afterEach(() => {
jest.useRealTimers();
});
async function computeSnapshot({
fileName,
systemTime
}: {
fileName: string;
systemTime: string;
}) {
jest.useFakeTimers().setSystemTime(parseDate(systemTime).getTime());
const { activities, userCurrency } = loadActivitiesFromExportFile(fileName);
return portfolioCalculatorFactory
.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROI,
currency: userCurrency,
userId: userDummyData.id
})
.computeSnapshot();
}
describe('get current positions', () => {
it('with NOVN.SW buy and sell', async () => {
const portfolioSnapshot = await computeSnapshot({
fileName: 'novn-buy-and-sell.json',
systemTime: '2022-04-11'
});
// Buy: 2 * 75.8 = 151.6, the money is invested for the whole period,
// thus the ROI equals the ROAI
expect(portfolioSnapshot.positions).toMatchObject([
{
netPerformance: new Big('19.86'),
netPerformancePercentage: new Big('19.86').div('151.6'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('19.86').div('151.6')
}
}
]);
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({
netPerformanceInPercentage: new Big('19.86').div('151.6').toNumber(),
totalBuyValue: 151.6
});
});
it('with NOVN.SW buy and sell partially', async () => {
const portfolioSnapshot = await computeSnapshot({
fileName: 'novn-buy-and-sell-partially.json',
systemTime: '2022-04-11'
});
// The partial sell does not reduce the total buy value of 151.6
expect(portfolioSnapshot.positions).toMatchObject([
{
netPerformance: new Big('17.68'),
netPerformancePercentage: new Big('17.68').div('151.6'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('17.68').div('151.6')
}
}
]);
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({
totalBuyValue: 151.6
});
});
it('with BALN.SW buy and buy', async () => {
const portfolioSnapshot = await computeSnapshot({
fileName: 'baln-buy-and-buy.json',
systemTime: '2021-12-18'
});
// Buys: 2 * 142.9 + 2 * 136.6 = 559
expect(portfolioSnapshot.positions).toMatchObject([
{
netPerformance: new Big('33.4'),
netPerformancePercentage: new Big('33.4').div('559'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('33.4').div('559')
}
}
]);
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({
totalBuyValue: 559
});
});
it('with JNUG buy and sell and buy and sell', async () => {
const portfolioSnapshot = await computeSnapshot({
fileName: 'jnug-buy-and-sell-and-buy-and-sell.json',
systemTime: '2025-12-28'
});
// Buys: 9 * 209.45 + 10 * 204.11 = 3926.15, the money of the first
// sell is invested again and thus counts twice
expect(portfolioSnapshot.positions).toMatchObject([
{
netPerformance: new Big('39.95'),
netPerformancePercentage: new Big('39.95').div('3926.15'),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('39.95').div('3926.15')
}
}
]);
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({
totalBuyValue: 3926.15
});
});
it('with BTCUSD short sell', async () => {
const portfolioSnapshot = await computeSnapshot({
fileName: 'btcusd-short.json',
systemTime: '2022-01-14'
});
// A short position has no buy, thus the ROI is 0
expect(portfolioSnapshot.positions).toMatchObject([
{
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: { max: new Big(0) }
}
]);
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({
netPerformanceInPercentage: 0,
totalBuyValue: 0
});
});
it('with no activities', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime());
const portfolioSnapshot = await portfolioCalculatorFactory
.createCalculator({
activities: [],
calculationType: PerformanceCalculationType.ROI,
currency: 'CHF',
userId: userDummyData.id
})
.computeSnapshot();
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big(0),
historicalData: [],
positions: [],
totalInvestment: new Big(0)
});
});
});
});

177
apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-googl-buy-with-dividend.spec.ts

@ -0,0 +1,177 @@
import {
activityDummyData,
assetProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock';
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 { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper';
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;
})
};
});
jest.mock(
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service',
() => {
return {
ExchangeRateDataService: jest.fn().mockImplementation(() => {
return ExchangeRateDataServiceMock;
})
};
}
);
describe('RoiPortfolioCalculator', () => {
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeEach(() => {
PortfolioSnapshotServiceMock.reset();
RedisCacheServiceMock.reset();
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null, null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it('with GOOGL buy and dividend', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2023-07-10').getTime());
const activities: Activity[] = [
{
...activityDummyData,
assetProfile: {
...assetProfileDummyData,
currency: 'USD',
dataSource: 'YAHOO',
name: 'Alphabet Inc.',
symbol: 'GOOGL'
},
date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 1,
feeInBaseCurrency: 0.9238,
quantity: 1,
type: 'BUY',
unitPriceInAssetProfileCurrency: 89.12
},
{
...activityDummyData,
assetProfile: {
...assetProfileDummyData,
currency: 'USD',
dataSource: 'YAHOO',
name: 'Alphabet Inc.',
symbol: 'GOOGL'
},
date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
type: 'DIVIDEND',
unitPriceInAssetProfileCurrency: 5
}
];
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROI,
currency: 'CHF',
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
// The buy is valued at the current exchange rate (0.8854) and with the
// currency effect at the exchange rate of the buy date (0.9238)
expect(portfolioSnapshot.positions).toMatchObject([
{
netPerformancePercentage: new Big('31.33')
.mul(0.8854)
.div(new Big('89.12').mul(0.8854)),
netPerformancePercentageWithCurrencyEffectMap: {
max: new Big('24.470974').div(new Big('89.12').mul(0.9238))
}
}
]);
expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({
dividendInPercentageWithCurrencyEffect: new Big('4.619')
.div(new Big('89.12').mul(0.9238))
.toNumber(),
totalBuyValue: new Big('89.12').mul(0.8854).toNumber(),
totalBuyValueWithCurrencyEffect: new Big('89.12').mul(0.9238).toNumber()
});
// 188 days between the first activity (2023-01-03) and today (2023-07-10)
expect(portfolioSnapshot.dividendYieldPercentWithCurrencyEffect).toEqual(
getAnnualizedPerformancePercent({
daysInMarket: 188,
netPerformancePercentage: new Big('4.619').div(
new Big('89.12').mul(0.9238)
)
})
);
});
});
});
Loading…
Cancel
Save