implement support for buy-sell(-buy) scenario (#262)

Co-authored-by: Valentin Zickner <github@zickner.ch>
pull/263/head
Valentin Zickner 4 years ago committed by GitHub
parent 218efbb5bd
commit dfcf826b4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -396,115 +396,9 @@ describe('PortfolioCalculator', () => {
const portfolioItemsAtTransactionPoints = const portfolioItemsAtTransactionPoints =
portfolioCalculator.getTransactionPoints(); portfolioCalculator.getTransactionPoints();
expect(portfolioItemsAtTransactionPoints).toEqual([ expect(portfolioItemsAtTransactionPoints).toEqual(
{ transactionPointsBuyAndSell
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: '2019-09-01',
items: [
{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
},
{
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: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
},
{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}
]
},
{
date: '2020-08-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
}
]
}
]);
}); });
it('with mixed symbols', () => { it('with mixed symbols', () => {
@ -740,6 +634,138 @@ describe('PortfolioCalculator', () => {
}); });
}); });
it('with buy and sell', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
);
portfolioCalculator.setTransactionPoints(transactionPointsBuyAndSell);
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2020, 9, 24)).getTime()); // 2020-10-24
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2019-01-01')
);
spy.mockRestore();
expect(currentPositions).toEqual({
hasErrors: false,
currentValue: new Big('4871.5'),
grossPerformance: new Big('240.4'),
grossPerformancePercentage: new Big('0.01104605615757711361'),
totalInvestment: new Big('4460.95'),
positions: [
{
averagePrice: new Big('0'),
currency: 'USD',
firstBuyDate: '2019-09-01',
grossPerformance: new Big('0'),
grossPerformancePercentage: new Big('0'),
investment: new Big('0'),
marketPrice: 2021.99,
quantity: new Big('0'),
symbol: 'AMZN',
transactionCount: 2
},
{
averagePrice: new Big('178.438'),
currency: 'USD',
firstBuyDate: '2019-02-01',
grossPerformance: new Big('240.4'),
grossPerformancePercentage: new Big(
'0.08839407904876477101219019935616297754969945667391763908415656216989674494965785538864363782688167989866968512455219637257546280462751601552'
),
investment: new Big('4460.95'),
marketPrice: 194.86,
quantity: new Big('25'),
symbol: 'VTI',
transactionCount: 5
}
]
});
});
it('with buy, sell, buy', async () => {
const portfolioCalculator = new PortfolioCalculator(
currentRateService,
Currency.USD
);
portfolioCalculator.setTransactionPoints([
{
date: '2019-09-01',
items: [
{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('805.9'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
}
]
},
{
date: '2020-08-02',
items: [
{
quantity: new Big('0'),
symbol: 'VTI',
investment: new Big('0'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 2
}
]
},
{
date: '2021-02-01',
items: [
{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('1013.9'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 3
}
]
}
]);
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date(Date.UTC(2021, 7, 1)).getTime()); // 2021-08-01
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2019-02-01')
);
spy.mockRestore();
expect(currentPositions).toEqual({
hasErrors: false,
currentValue: new Big('1086.7'),
grossPerformance: new Big('207.6'),
grossPerformancePercentage: new Big('0.2516103956224511062'),
totalInvestment: new Big('1013.9'),
positions: [
{
averagePrice: new Big('202.78'),
currency: 'USD',
firstBuyDate: '2019-09-01',
grossPerformance: new Big('207.6'),
grossPerformancePercentage: new Big(
'0.2516103956224511061954915466429950404846'
),
investment: new Big('1013.9'),
marketPrice: 217.34,
quantity: new Big('5'),
symbol: 'VTI',
transactionCount: 3
}
]
});
});
it('with performance since Jan 1st, 2020', async () => { it('with performance since Jan 1st, 2020', async () => {
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator(
currentRateService, currentRateService,
@ -1776,3 +1802,137 @@ const ordersVTITransactionPoints: TransactionPoint[] = [
] ]
} }
]; ];
const transactionPointsBuyAndSell = [
{
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: '2019-09-01',
items: [
{
quantity: new Big('5'),
symbol: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
},
{
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: 'AMZN',
investment: new Big('10109.95'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 1
},
{
quantity: new Big('5'),
symbol: 'VTI',
investment: new Big('652.55'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 3
}
]
},
{
date: '2020-08-02',
items: [
{
quantity: new Big('0'),
symbol: 'AMZN',
investment: new Big('0'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 2
},
{
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('0'),
symbol: 'AMZN',
investment: new Big('0'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 2
},
{
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('0'),
symbol: 'AMZN',
investment: new Big('0'),
currency: Currency.USD,
firstBuyDate: '2019-09-01',
transactionCount: 2
},
{
quantity: new Big('25'),
symbol: 'VTI',
investment: new Big('4460.95'),
currency: Currency.USD,
firstBuyDate: '2019-02-01',
transactionCount: 5
}
]
}
];

@ -52,16 +52,19 @@ export class PortfolioCalculator {
const factor = this.getFactor(order.type); const factor = this.getFactor(order.type);
const unitPrice = new Big(order.unitPrice); const unitPrice = new Big(order.unitPrice);
if (oldAccumulatedSymbol) { if (oldAccumulatedSymbol) {
const newQuantity = order.quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
currentTransactionPointItem = { currentTransactionPointItem = {
currency: order.currency, currency: order.currency,
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
investment: unitPrice investment: newQuantity.eq(0)
.mul(order.quantity) ? new Big(0)
.mul(factor) : unitPrice
.add(oldAccumulatedSymbol.investment), .mul(order.quantity)
quantity: order.quantity .mul(factor)
.mul(factor) .add(oldAccumulatedSymbol.investment),
.plus(oldAccumulatedSymbol.quantity), quantity: newQuantity,
symbol: order.symbol, symbol: order.symbol,
transactionCount: oldAccumulatedSymbol.transactionCount + 1 transactionCount: oldAccumulatedSymbol.transactionCount + 1
}; };
@ -82,11 +85,7 @@ export class PortfolioCalculator {
const newItems = items.filter( const newItems = items.filter(
(transactionPointItem) => transactionPointItem.symbol !== order.symbol (transactionPointItem) => transactionPointItem.symbol !== order.symbol
); );
if (!currentTransactionPointItem.quantity.eq(0)) { newItems.push(currentTransactionPointItem);
newItems.push(currentTransactionPointItem);
} else {
delete symbols[order.symbol];
}
newItems.sort((a, b) => a.symbol.localeCompare(b.symbol)); newItems.sort((a, b) => a.symbol.localeCompare(b.symbol));
if (lastDate !== currentDate || lastTransactionPoint === null) { if (lastDate !== currentDate || lastTransactionPoint === null) {
lastTransactionPoint = { lastTransactionPoint = {
@ -231,31 +230,32 @@ export class PortfolioCalculator {
if (i === firstIndex || !initialValues[item.symbol]) { if (i === firstIndex || !initialValues[item.symbol]) {
initialValues[item.symbol] = initialValue; initialValues[item.symbol] = initialValue;
} }
if (!initialValue) { if (!item.quantity.eq(0)) {
invalidSymbols.push(item.symbol); if (!initialValue) {
hasErrors = true; invalidSymbols.push(item.symbol);
console.error( hasErrors = true;
`Missing value for symbol ${item.symbol} at ${currentDate}` console.error(
); `Missing value for symbol ${item.symbol} at ${currentDate}`
continue; );
} continue;
}
const cashFlow = lastInvestment; const cashFlow = lastInvestment;
const endValue = marketSymbolMap[nextDate][item.symbol].mul( const endValue = marketSymbolMap[nextDate][item.symbol].mul(
item.quantity item.quantity
); );
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow)); const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
holdingPeriodReturns[item.symbol] = holdingPeriodReturns[item.symbol] =
oldHoldingPeriodReturn.mul(holdingPeriodReturn); oldHoldingPeriodReturn.mul(holdingPeriodReturn);
let oldGrossPerformance = grossPerformance[item.symbol]; let oldGrossPerformance = grossPerformance[item.symbol];
if (!oldGrossPerformance) { if (!oldGrossPerformance) {
oldGrossPerformance = new Big(0); oldGrossPerformance = new Big(0);
}
const currentPerformance = endValue.minus(investedValue);
grossPerformance[item.symbol] =
oldGrossPerformance.plus(currentPerformance);
} }
const currentPerformance = endValue.minus(investedValue);
grossPerformance[item.symbol] =
oldGrossPerformance.plus(currentPerformance);
lastInvestments[item.symbol] = item.investment; lastInvestments[item.symbol] = item.investment;
lastQuantities[item.symbol] = item.quantity; lastQuantities[item.symbol] = item.quantity;
} }
@ -267,7 +267,9 @@ export class PortfolioCalculator {
const marketValue = marketSymbolMap[todayString]?.[item.symbol]; const marketValue = marketSymbolMap[todayString]?.[item.symbol];
const isValid = invalidSymbols.indexOf(item.symbol) === -1; const isValid = invalidSymbols.indexOf(item.symbol) === -1;
positions.push({ positions.push({
averagePrice: item.investment.div(item.quantity), averagePrice: item.quantity.eq(0)
? new Big(0)
: item.investment.div(item.quantity),
currency: item.currency, currency: item.currency,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: isValid grossPerformance: isValid
@ -398,7 +400,7 @@ export class PortfolioCalculator {
grossPerformance = grossPerformance.plus( grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance currentPosition.grossPerformance
); );
} else { } else if (!currentPosition.quantity.eq(0)) {
hasErrors = true; hasErrors = true;
} }
@ -411,7 +413,7 @@ export class PortfolioCalculator {
grossPerformancePercentage = grossPerformancePercentage.plus( grossPerformancePercentage = grossPerformancePercentage.plus(
currentPosition.grossPerformancePercentage.mul(currentInitialValue) currentPosition.grossPerformancePercentage.mul(currentInitialValue)
); );
} else { } else if (!currentPosition.quantity.eq(0)) {
console.error( console.error(
`Initial value is missing for symbol ${currentPosition.symbol}` `Initial value is missing for symbol ${currentPosition.symbol}`
); );

@ -358,9 +358,9 @@ export class PortfolioService {
(item) => item.symbol === aSymbol (item) => item.symbol === aSymbol
); );
if (currentSymbol) { if (currentSymbol) {
currentAveragePrice = currentSymbol.investment currentAveragePrice = currentSymbol.quantity.eq(0)
.div(currentSymbol.quantity) ? 0
.toNumber(); : currentSymbol.investment.div(currentSymbol.quantity).toNumber();
} }
historicalDataArray.push({ historicalDataArray.push({
@ -470,9 +470,10 @@ export class PortfolioService {
startDate startDate
); );
const symbols = currentPositions.positions.map( const positions = currentPositions.positions.filter(
(position) => position.symbol (item) => !item.quantity.eq(0)
); );
const symbols = positions.map((position) => position.symbol);
const [dataProviderResponses, symbolProfiles] = await Promise.all([ const [dataProviderResponses, symbolProfiles] = await Promise.all([
this.dataProviderService.get(symbols), this.dataProviderService.get(symbols),
@ -486,7 +487,7 @@ export class PortfolioService {
return { return {
hasErrors: currentPositions.hasErrors, hasErrors: currentPositions.hasErrors,
positions: currentPositions.positions.map((position) => { positions: positions.map((position) => {
return { return {
...position, ...position,
averagePrice: new Big(position.averagePrice).toNumber(), averagePrice: new Big(position.averagePrice).toNumber(),

Loading…
Cancel
Save