* Add test

* fix calculation for overall gross performance percentage

Co-authored-by: Valentin Zickner <github@zickner.ch>
pull/239/head
Thomas 3 years ago
parent 9c51a257ae
commit fb15cebb64

@ -7,7 +7,7 @@ import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timel
import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/core/interfaces/transaction-point.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator';
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { parseDate, resetHours } from '@ghostfolio/common/helper';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
@ -15,8 +15,7 @@ import {
differenceInCalendarDays, differenceInCalendarDays,
endOfDay, endOfDay,
isBefore, isBefore,
isSameDay, isSameDay
parse
} from 'date-fns'; } from 'date-fns';
function mockGetValue(symbol: string, date: Date) { function mockGetValue(symbol: string, date: Date) {
@ -25,7 +24,7 @@ function mockGetValue(symbol: string, date: Date) {
if (isSameDay(today, date)) { if (isSameDay(today, date)) {
return { marketPrice: 213.32 }; return { marketPrice: 213.32 };
} else { } else {
const startDate = parse('2019-02-01', DATE_FORMAT, new Date()); const startDate = parseDate('2019-02-01');
const daysInBetween = differenceInCalendarDays(date, startDate); const daysInBetween = differenceInCalendarDays(date, startDate);
const marketPrice = new Big('144.38').plus( const marketPrice = new Big('144.38').plus(
@ -44,11 +43,23 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 1.097884981 }; // 1192328 / 1086022.689344541 return { marketPrice: 1.097884981 }; // 1192328 / 1086022.689344541
} }
return { marketPrice: 0 };
} else if (symbol === 'SPA') {
if (isSameDay(parseDate('2013-12-31'), date)) {
return { marketPrice: 1.025 }; // 205 / 200
}
return { marketPrice: 0 };
} else if (symbol === 'SPB') {
if (isSameDay(parseDate('2013-12-31'), date)) {
return { marketPrice: 1.04 }; // 312 / 300
}
return { marketPrice: 0 }; return { marketPrice: 0 };
} else if (symbol === 'TSLA') { } else if (symbol === 'TSLA') {
if (isSameDay(parse('2021-07-26', DATE_FORMAT, new Date()), date)) { if (isSameDay(parseDate('2021-07-26'), date)) {
return { marketPrice: 657.62 }; return { marketPrice: 657.62 };
} else if (isSameDay(parse('2021-01-02', DATE_FORMAT, new Date()), date)) { } else if (isSameDay(parseDate('2021-01-02'), date)) {
return { marketPrice: 666.66 }; return { marketPrice: 666.66 };
} }
@ -617,7 +628,7 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26 .mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parse('2020-01-21', DATE_FORMAT, new Date()) parseDate('2020-01-21')
); );
spy.mockRestore(); spy.mockRestore();
@ -625,7 +636,7 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
currentValue: new Big('657.62'), currentValue: new Big('657.62'),
grossPerformance: new Big('-61.84'), grossPerformance: new Big('-61.84'),
grossPerformancePercentage: new Big('-0.08456342256692519389'), grossPerformancePercentage: new Big('-0.08595335390431712673'),
positions: [ positions: [
{ {
averagePrice: new Big('719.46'), averagePrice: new Big('719.46'),
@ -655,7 +666,7 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26 .mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parse('2021-01-01', DATE_FORMAT, new Date()) parseDate('2021-01-01')
); );
spy.mockRestore(); spy.mockRestore();
@ -663,7 +674,7 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
currentValue: new Big('657.62'), currentValue: new Big('657.62'),
grossPerformance: new Big('-61.84'), grossPerformance: new Big('-61.84'),
grossPerformancePercentage: new Big('-0.08456342256692519389'), grossPerformancePercentage: new Big('-0.08595335390431712673'),
positions: [ positions: [
{ {
averagePrice: new Big('719.46'), averagePrice: new Big('719.46'),
@ -693,7 +704,7 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26 .mockImplementation(() => new Date(Date.UTC(2021, 6, 26)).getTime()); // 2021-07-26
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parse('2021-01-02', DATE_FORMAT, new Date()) parseDate('2021-01-02')
); );
spy.mockRestore(); spy.mockRestore();
@ -701,7 +712,7 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
currentValue: new Big('657.62'), currentValue: new Big('657.62'),
grossPerformance: new Big('-9.04'), grossPerformance: new Big('-9.04'),
grossPerformancePercentage: new Big('-0.01206012060120601206'), grossPerformancePercentage: new Big('-0.01356013560135601356'),
positions: [ positions: [
{ {
averagePrice: new Big('719.46'), averagePrice: new Big('719.46'),
@ -731,7 +742,7 @@ describe('PortfolioCalculator', () => {
.spyOn(Date, 'now') .spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24 .mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parse('2019-01-01', DATE_FORMAT, new Date()) parseDate('2019-01-01')
); );
spy.mockRestore(); spy.mockRestore();
@ -739,7 +750,7 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
currentValue: new Big('4871.5'), currentValue: new Big('4871.5'),
grossPerformance: new Big('240.4'), grossPerformance: new Big('240.4'),
grossPerformancePercentage: new Big('0.08908669575467971768'), grossPerformancePercentage: new Big('0.08839407904876477102'),
positions: [ positions: [
{ {
averagePrice: new Big('178.438'), averagePrice: new Big('178.438'),
@ -811,7 +822,7 @@ describe('PortfolioCalculator', () => {
// gross performance percentage: 1.100526008 * 1.158880728 = 1.275378381 => 27.5378381 % // gross performance percentage: 1.100526008 * 1.158880728 = 1.275378381 => 27.5378381 %
const currentPositions = await portfolioCalculator.getCurrentPositions( const currentPositions = await portfolioCalculator.getCurrentPositions(
parse('2020-01-01', DATE_FORMAT, new Date()) parseDate('2020-01-01')
); );
spy.mockRestore(); spy.mockRestore();
@ -819,7 +830,7 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
currentValue: new Big('3897.2'), currentValue: new Big('3897.2'),
grossPerformance: new Big('303.2'), grossPerformance: new Big('303.2'),
grossPerformancePercentage: new Big('0.2759628350186678759'), grossPerformancePercentage: new Big('0.27537838148272398344'),
positions: [ positions: [
{ {
averagePrice: new Big('146.185'), averagePrice: new Big('146.185'),
@ -892,7 +903,7 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
currentValue: new Big('1192327.999656600298238721'), currentValue: new Big('1192327.999656600298238721'),
grossPerformance: new Big('92327.999656600898394721'), grossPerformance: new Big('92327.999656600898394721'),
grossPerformancePercentage: new Big('0.09788598099999947809'), grossPerformancePercentage: new Big('0.09788498099999947809'),
positions: [ positions: [
{ {
averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542 averagePrice: new Big('1.01287018290924923237'), // 1'100'000 / 1'086'022.689344542
@ -910,6 +921,108 @@ describe('PortfolioCalculator', () => {
] ]
}); });
}); });
/**
* Source: https://www.chsoft.ch/en/assets/Dateien/files/PDF/ePoca/en/Practical%20Performance%20Calculation.pdf
*/
it('with example from chsoft.ch: Performance of a Combination of Investments', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.CHF
);
portfolioCalculator.setTransactionPoints([
{
date: '2012-12-31',
items: [
{
name: 'Sub Portfolio A',
quantity: new Big('200'),
symbol: 'SPA',
investment: new Big('200'),
currency: Currency.CHF,
firstBuyDate: '2012-12-31',
transactionCount: 1
},
{
name: 'Sub Portfolio B',
quantity: new Big('300'),
symbol: 'SPB',
investment: new Big('300'),
currency: Currency.CHF,
firstBuyDate: '2012-12-31',
transactionCount: 1
}
]
},
{
date: '2013-12-31',
items: [
{
name: 'Sub Portfolio A',
quantity: new Big('200'),
symbol: 'SPA',
investment: new Big('200'),
currency: Currency.CHF,
firstBuyDate: '2012-12-31',
transactionCount: 1
},
{
name: 'Sub Portfolio B',
quantity: new Big('300'),
symbol: 'SPB',
investment: new Big('300'),
currency: Currency.CHF,
firstBuyDate: '2012-12-31',
transactionCount: 1
}
]
}
]);
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2013, 11, 31)).getTime()); // 2013-12-31
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2012-12-31')
);
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('517'),
grossPerformance: new Big('17'), // 517 - 500
grossPerformancePercentage: new Big('0.034'), // ((200 * 0.025) + (300 * 0.04)) / (200 + 300) = 3.4%
hasErrors: false,
positions: [
{
averagePrice: new Big('1'),
firstBuyDate: '2012-12-31',
quantity: new Big('200'),
symbol: 'SPA',
investment: new Big('200'),
marketPrice: 1.025, // 205 / 200
transactionCount: 1,
grossPerformance: new Big('5'), // 205 - 200
grossPerformancePercentage: new Big('0.025'),
name: 'Sub Portfolio A',
currency: 'CHF'
},
{
averagePrice: new Big('1'),
firstBuyDate: '2012-12-31',
quantity: new Big('300'),
symbol: 'SPB',
investment: new Big('300'),
marketPrice: 1.04, // 312 / 300
transactionCount: 1,
grossPerformance: new Big('12'), // 312 - 300
grossPerformancePercentage: new Big('0.04'),
name: 'Sub Portfolio B',
currency: 'CHF'
}
]
});
});
}); });
describe('calculate timeline', () => { describe('calculate timeline', () => {

@ -297,48 +297,15 @@ export class PortfolioCalculator {
transactionCount: item.transactionCount transactionCount: item.transactionCount
}); });
} }
const overall = this.calculateOverallGrossPerformance(
let currentValue = new Big(0); positions,
let overallGrossPerformance = new Big(0); initialValues
let grossPerformancePercentage = new Big(1); );
let completeInitialValue = new Big(0);
for (const currentPosition of positions) {
currentValue = currentValue.add(
new Big(currentPosition.marketPrice).mul(currentPosition.quantity)
);
if (currentPosition.grossPerformance) {
overallGrossPerformance = overallGrossPerformance.plus(
currentPosition.grossPerformance
);
} else {
hasErrors = true;
}
if (
currentPosition.grossPerformancePercentage &&
initialValues[currentPosition.symbol]
) {
const currentInitialValue = initialValues[currentPosition.symbol];
completeInitialValue = completeInitialValue.plus(currentInitialValue);
grossPerformancePercentage = grossPerformancePercentage.plus(
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
);
} else {
console.log(initialValues);
console.error(
'initial value is missing for symbol',
currentPosition.symbol
);
hasErrors = true;
}
}
return { return {
hasErrors, ...overall,
positions, hasErrors: hasErrors || overall.hasErrors,
grossPerformance: overallGrossPerformance, positions
grossPerformancePercentage:
grossPerformancePercentage.div(completeInitialValue),
currentValue
}; };
} }
@ -404,6 +371,53 @@ export class PortfolioCalculator {
return flatten(timelinePeriods); return flatten(timelinePeriods);
} }
private calculateOverallGrossPerformance(
positions: TimelinePosition[],
initialValues: { [p: string]: Big }
) {
let hasErrors = false;
let currentValue = new Big(0);
let grossPerformance = new Big(0);
let grossPerformancePercentage = new Big(0);
let completeInitialValue = new Big(0);
for (const currentPosition of positions) {
currentValue = currentValue.add(
new Big(currentPosition.marketPrice).mul(currentPosition.quantity)
);
if (currentPosition.grossPerformance) {
grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance
);
} else {
hasErrors = true;
}
if (
currentPosition.grossPerformancePercentage &&
initialValues[currentPosition.symbol]
) {
const currentInitialValue = initialValues[currentPosition.symbol];
completeInitialValue = completeInitialValue.plus(currentInitialValue);
grossPerformancePercentage = grossPerformancePercentage.plus(
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
);
} else {
console.error(
'initial value is missing for symbol',
currentPosition.symbol
);
hasErrors = true;
}
}
return {
currentValue,
grossPerformance,
grossPerformancePercentage:
grossPerformancePercentage.div(completeInitialValue),
hasErrors
};
}
private async getTimePeriodForDate( private async getTimePeriodForDate(
j: number, j: number,
startDate: Date, startDate: Date,

Loading…
Cancel
Save