Add test (#237)
* Add test * fix calculation for overall gross performance percentage Co-authored-by: Valentin Zickner <github@zickner.ch>
This commit is contained in:
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…
x
Reference in New Issue
Block a user