Browse Source

implement getCurrentPositions of PortfolioCalculator

pull/239/head
Valentin Zickner 3 years ago
committed by Thomas
parent
commit
7538133d09
  1. 304
      apps/api/src/app/core/portfolio-calculator.spec.ts
  2. 69
      apps/api/src/app/core/portfolio-calculator.ts

304
apps/api/src/app/core/portfolio-calculator.spec.ts

@ -1,16 +1,39 @@
import { PortfolioCalculator, PortfolioOrder } from '@ghostfolio/api/app/core/portfolio-calculator'; import { PortfolioCalculator, PortfolioOrder } from '@ghostfolio/api/app/core/portfolio-calculator';
import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; import { CurrentRateService, GetValueParams } from '@ghostfolio/api/app/core/current-rate.service';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { OrderType } from '@ghostfolio/api/models/order-type'; import { OrderType } from '@ghostfolio/api/models/order-type';
import Big from 'big.js'; import Big from 'big.js';
function toYearMonthDay(date: Date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return [year, month, day];
}
function dateEqual(date1: Date, date2: Date) {
const date1Converted = toYearMonthDay(date1);
const date2Converted = toYearMonthDay(date2);
return date1Converted[0] === date2Converted[0]&&
date1Converted[1] === date2Converted[1] &&
date1Converted[2] === date2Converted[2]
}
jest.mock('./current-rate.service.ts', () => { jest.mock('./current-rate.service.ts', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return { return {
getValue: (date: Date, symbol: string, currency: Currency) => { getValue: ({date, symbol, currency, userCurrency}: GetValueParams) => {
return 4; const today = new Date();
if (dateEqual(today, date) && symbol === 'VTI') {
return Promise.resolve(new Big('213.32'));
}
return Promise.resolve(new Big('0'));
} }
}; };
}) })
@ -26,56 +49,11 @@ describe('PortfolioCalculator', () => {
describe('calculate transaction points', () => { describe('calculate transaction points', () => {
it('with orders of only one symbol', () => { it('with orders of only one symbol', () => {
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, ordersVTI); const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); portfolioCalculator.computeTransactionPoints(ordersVTI);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([ expect(portfolioItemsAtTransactionPoints).toEqual(ordersVTITransactionPoints);
{
date: '2019-02-01',
items: [{
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD
}]
},
{
date: '2019-08-03',
items: [{
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD
}]
},
{
date: '2020-02-02',
items: [{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD
}]
},
{
date: '2021-02-01',
items: [{
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD
}]
},
{
date: '2021-08-01',
items: [{
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD
}]
}
]);
}); });
it('with two orders at the same day of the same type', () => { it('with two orders at the same day of the same type', () => {
@ -90,8 +68,9 @@ describe('PortfolioCalculator', () => {
currency: Currency.USD currency: Currency.USD
} }
]; ];
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders); const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); portfolioCalculator.computeTransactionPoints(orders);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([ expect(portfolioItemsAtTransactionPoints).toEqual([
{ {
@ -100,7 +79,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 1
}] }]
}, },
{ {
@ -109,7 +90,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 2
}] }]
}, },
{ {
@ -118,7 +101,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}] }]
}, },
{ {
@ -127,7 +112,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('35'), quantity: new Big('35'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('6627.05'), investment: new Big('6627.05'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 5
}] }]
}, },
{ {
@ -136,7 +123,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('45'), quantity: new Big('45'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('8403.95'), investment: new Big('8403.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 6
}] }]
} }
]); ]);
@ -154,8 +143,9 @@ describe('PortfolioCalculator', () => {
currency: Currency.USD currency: Currency.USD
} }
]; ];
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders); const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); portfolioCalculator.computeTransactionPoints(orders);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([ expect(portfolioItemsAtTransactionPoints).toEqual([
{ {
@ -164,7 +154,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 1
}] }]
}, },
{ {
@ -173,7 +165,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 2
}] }]
}, },
{ {
@ -182,12 +176,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 2
}] }]
}, },
{ {
@ -196,12 +194,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}] }]
}, },
{ {
@ -210,12 +212,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('15'), quantity: new Big('15'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2684.05'), investment: new Big('2684.05'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 4
}] }]
}, },
{ {
@ -224,12 +230,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('25'), quantity: new Big('25'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 5
}] }]
} }
]); ]);
@ -255,8 +265,9 @@ describe('PortfolioCalculator', () => {
currency: Currency.USD currency: Currency.USD
} }
]; ];
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders); const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); portfolioCalculator.computeTransactionPoints(orders);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([ expect(portfolioItemsAtTransactionPoints).toEqual([
{ {
@ -265,7 +276,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('10'), quantity: new Big('10'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('1443.8'), investment: new Big('1443.8'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 1
}] }]
}, },
{ {
@ -274,7 +287,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 2
}] }]
}, },
{ {
@ -283,12 +298,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('20'), quantity: new Big('20'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2923.7'), investment: new Big('2923.7'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 2
}] }]
}, },
{ {
@ -297,12 +316,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}] }]
}, },
{ {
@ -311,7 +334,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('652.55'), investment: new Big('652.55'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}] }]
}, },
{ {
@ -320,7 +345,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('15'), quantity: new Big('15'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('2684.05'), investment: new Big('2684.05'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 4
}] }]
}, },
{ {
@ -329,15 +356,18 @@ describe('PortfolioCalculator', () => {
quantity: new Big('25'), quantity: new Big('25'),
symbol: 'VTI', symbol: 'VTI',
investment: new Big('4460.95'), investment: new Big('4460.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 5
}] }]
} }
]); ]);
}); });
it('with mixed symbols', () => { it('with mixed symbols', () => {
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, ordersMixedSymbols); const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); portfolioCalculator.computeTransactionPoints(ordersMixedSymbols);
const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([ expect(portfolioItemsAtTransactionPoints).toEqual([
{ {
@ -346,7 +376,9 @@ describe('PortfolioCalculator', () => {
quantity: new Big('50'), quantity: new Big('50'),
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('2148.5'), investment: new Big('2148.5'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2017-01-03',
transactionCount: 1
}] }]
}, },
{ {
@ -355,12 +387,16 @@ describe('PortfolioCalculator', () => {
quantity: new Big('0.5614682'), quantity: new Big('0.5614682'),
symbol: 'BTCUSD', symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'), investment: new Big('1999.9999999999998659756'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2017-07-01',
transactionCount: 1
}, { }, {
quantity: new Big('50'), quantity: new Big('50'),
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('2148.5'), investment: new Big('2148.5'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2017-01-03',
transactionCount: 1
}] }]
}, },
{ {
@ -369,25 +405,55 @@ describe('PortfolioCalculator', () => {
quantity: new Big('5'), quantity: new Big('5'),
symbol: 'AMZN', symbol: 'AMZN',
investment: new Big('10109.95'), investment: new Big('10109.95'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2018-09-01',
transactionCount: 1
}, { }, {
quantity: new Big('0.5614682'), quantity: new Big('0.5614682'),
symbol: 'BTCUSD', symbol: 'BTCUSD',
investment: new Big('1999.9999999999998659756'), investment: new Big('1999.9999999999998659756'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2017-07-01',
transactionCount: 1
}, { }, {
quantity: new Big('50'), quantity: new Big('50'),
symbol: 'TSLA', symbol: 'TSLA',
investment: new Big('2148.5'), investment: new Big('2148.5'),
currency: Currency.USD currency: Currency.USD,
firstBuyDate: '2017-01-03',
transactionCount: 1
}] }]
} }
]); ]);
}); });
}); });
describe('get current positions', () => {
it('with just VTI', async () => {
const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD);
portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints);
const currentPositions = await portfolioCalculator.getCurrentPositions();
expect(currentPositions).toEqual({
// eslint-disable-next-line @typescript-eslint/naming-convention
VTI: {
averagePrice: new Big('178.438'),
firstBuyDate: '2019-02-01',
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
marketPrice: new Big('213.32'),
transactionCount: 5
}
}); });
})
})
});
const ordersMixedSymbols: PortfolioOrder[] = [ const ordersMixedSymbols: PortfolioOrder[] = [
{ {
date: '2017-01-03', date: '2017-01-03',
@ -457,3 +523,61 @@ const ordersVTI: PortfolioOrder[] = [
currency: Currency.USD currency: Currency.USD
} }
]; ];
const ordersVTITransactionPoints = [
{
date: '2019-02-01',
items: [{
quantity: new Big('10'),
symbol: 'VTI',
investment: new Big('1443.8'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 1
}]
},
{
date: '2019-08-03',
items: [{
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 2
}]
},
{
date: '2020-02-02',
items: [{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}]
},
{
date: '2021-02-01',
items: [{
quantity: new Big('15'),
symbol: 'VTI',
investment: new Big('2684.05'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 4
}]
},
{
date: '2021-08-01',
items: [{
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 5
}]
}
];

69
apps/api/src/app/core/portfolio-calculator.ts

@ -9,10 +9,8 @@ export class PortfolioCalculator {
constructor( constructor(
private currentRateService: CurrentRateService, private currentRateService: CurrentRateService,
private currency: Currency, private currency: Currency
orders: PortfolioOrder[]
) { ) {
this.computeTransactionPoints(orders);
} }
addOrder(order: PortfolioOrder): void { addOrder(order: PortfolioOrder): void {
@ -23,19 +21,7 @@ export class PortfolioCalculator {
} }
getPortfolioItemsAtTransactionPoints(): TransactionPoint[] { computeTransactionPoints(orders: PortfolioOrder[]) {
return this.transactionPoints;
}
getCurrentPositions(): { [symbol: string]: TimelinePosition } {
return {};
}
calculateTimeline(timelineSpecification: TimelineSpecification[], endDate: Date): TimelinePeriod[] {
return null;
}
private computeTransactionPoints(orders: PortfolioOrder[]) {
orders.sort((a, b) => a.date.localeCompare(b.date)); orders.sort((a, b) => a.date.localeCompare(b.date));
this.transactionPoints = []; this.transactionPoints = [];
@ -56,14 +42,18 @@ export class PortfolioCalculator {
quantity: order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity), quantity: order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity),
symbol: order.symbol, symbol: order.symbol,
investment: unitPrice.mul(order.quantity).mul(factor).add(oldAccumulatedSymbol.investment), investment: unitPrice.mul(order.quantity).mul(factor).add(oldAccumulatedSymbol.investment),
currency: order.currency currency: order.currency,
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
transactionCount: oldAccumulatedSymbol.transactionCount + 1
}; };
} else { } else {
currentTransactionPointItem = { currentTransactionPointItem = {
quantity: order.quantity.mul(factor), quantity: order.quantity.mul(factor),
symbol: order.symbol, symbol: order.symbol,
investment: unitPrice.mul(order.quantity).mul(factor), investment: unitPrice.mul(order.quantity).mul(factor),
currency: order.currency currency: order.currency,
firstBuyDate: order.date,
transactionCount: 1
}; };
} }
@ -90,6 +80,47 @@ export class PortfolioCalculator {
} }
} }
setTransactionPoints(transactionPoints: TransactionPoint[]) {
this.transactionPoints = transactionPoints;
}
getTransactionPoints(): TransactionPoint[] {
return this.transactionPoints;
}
async getCurrentPositions(): Promise<{ [symbol: string]: TimelinePosition }> {
if (!this.transactionPoints?.length) {
return {};
}
const lastTransactionPoint = this.transactionPoints[this.transactionPoints.length - 1];
const result: { [symbol: string]: TimelinePosition } = {};
for (const item of lastTransactionPoint.items) {
const marketPrice = await this.currentRateService.getValue({
date: new Date(),
symbol: item.symbol,
currency: item.currency,
userCurrency: this.currency
});
result[item.symbol] = {
averagePrice: item.investment.div(item.quantity),
firstBuyDate: item.firstBuyDate,
quantity: item.quantity,
symbol: item.symbol,
investment: item.investment,
marketPrice: marketPrice,
transactionCount: item.transactionCount
};
}
return result;
}
calculateTimeline(timelineSpecification: TimelineSpecification[], endDate: Date): TimelinePeriod[] {
return null;
}
private getFactor(type: OrderType) { private getFactor(type: OrderType) {
let factor: number; let factor: number;
switch (type) { switch (type) {
@ -118,6 +149,8 @@ interface TransactionPointSymbol {
symbol: string; symbol: string;
investment: Big; investment: Big;
currency: Currency; currency: Currency;
firstBuyDate: string;
transactionCount: number;
} }
interface TimelinePosition { interface TimelinePosition {

Loading…
Cancel
Save