add error handling for current positions

pull/239/head
Valentin Zickner 3 years ago committed by Thomas
parent f65a108436
commit 34c13c80ec

@ -626,23 +626,25 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
// eslint-disable-next-line @typescript-eslint/naming-convention
VTI: {
averagePrice: new Big('178.438'),
currency: 'USD',
firstBuyDate: '2019-02-01',
// see next test for details about how to calculate this
grossPerformance: new Big('265.2'),
grossPerformancePercentage: new Big(
'0.37322057787174066244232522865731355471028555367747465860626740684417274277219590953836818016777856'
),
investment: new Big('4460.95'),
marketPrice: 194.86,
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('25'),
symbol: 'VTI',
transactionCount: 5
}
hasErrors: false,
positions: [
{
averagePrice: new Big('178.438'),
currency: 'USD',
firstBuyDate: '2019-02-01',
// see next test for details about how to calculate this
grossPerformance: new Big('265.2'),
grossPerformancePercentage: new Big(
'0.37322057787174066244232522865731355471028555367747465860626740684417274277219590953836818016777856'
),
investment: new Big('4460.95'),
marketPrice: 194.86,
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
quantity: new Big('25'),
symbol: 'VTI',
transactionCount: 5
}
]
});
});
@ -700,21 +702,24 @@ describe('PortfolioCalculator', () => {
spy.mockRestore();
expect(currentPositions).toEqual({
VTI: {
averagePrice: new Big('146.185'),
firstBuyDate: '2019-02-01',
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
marketPrice: 194.86,
transactionCount: 2,
grossPerformance: new Big('303.2'),
grossPerformancePercentage: new Big(
'0.1388661601402688486251911721754180022242'
),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
currency: 'USD'
}
hasErrors: false,
positions: [
{
averagePrice: new Big('146.185'),
firstBuyDate: '2019-02-01',
quantity: new Big('20'),
symbol: 'VTI',
investment: new Big('2923.7'),
marketPrice: 194.86,
transactionCount: 2,
grossPerformance: new Big('303.2'),
grossPerformancePercentage: new Big(
'0.1388661601402688486251911721754180022242'
),
name: 'Vanguard Total Stock Market Index Fund ETF Shares',
currency: 'USD'
}
]
});
});
});

@ -106,16 +106,19 @@ export class PortfolioCalculator {
}
public async getCurrentPositions(start: Date): Promise<{
[symbol: string]: TimelinePosition;
hasErrors: boolean;
positions: TimelinePosition[];
}> {
if (!this.transactionPoints?.length) {
return {};
return {
hasErrors: false,
positions: []
};
}
const lastTransactionPoint =
this.transactionPoints[this.transactionPoints.length - 1];
const result: { [symbol: string]: TimelinePosition } = {};
// use Date.now() to use the mock for today
const today = new Date(Date.now());
@ -171,6 +174,7 @@ export class PortfolioCalculator {
);
}
let hasErrors = false;
const startString = format(start, DATE_FORMAT);
const holdingPeriodReturns: { [symbol: string]: Big } = {};
@ -178,12 +182,14 @@ export class PortfolioCalculator {
let todayString = format(today, DATE_FORMAT);
// in case no symbols are there for today, use yesterday
if (!marketSymbolMap[todayString]) {
hasErrors = true;
todayString = format(subDays(today, 1), DATE_FORMAT);
}
if (firstIndex > 0) {
firstIndex--;
}
const invalidSymbols = [];
for (let i = firstIndex; i < this.transactionPoints.length; i++) {
const currentDate =
i === firstIndex ? startString : this.transactionPoints[i].date;
@ -198,6 +204,22 @@ export class PortfolioCalculator {
if (!oldHoldingPeriodReturn) {
oldHoldingPeriodReturn = new Big(1);
}
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
invalidSymbols.push(item.symbol);
hasErrors = true;
console.error(
`Missing value for symbol ${item.symbol} at ${nextDate}`
);
continue;
}
if (!marketSymbolMap[currentDate]?.[item.symbol]) {
invalidSymbols.push(item.symbol);
hasErrors = true;
console.error(
`Missing value for symbol ${item.symbol} at ${currentDate}`
);
continue;
}
holdingPeriodReturns[item.symbol] = oldHoldingPeriodReturn.mul(
marketSymbolMap[nextDate][item.symbol].div(
marketSymbolMap[currentDate][item.symbol]
@ -215,26 +237,31 @@ export class PortfolioCalculator {
}
}
const positions: TimelinePosition[] = [];
for (const item of lastTransactionPoint.items) {
const marketValue = marketSymbolMap[todayString][item.symbol];
result[item.symbol] = {
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
const isValid = invalidSymbols.indexOf(item.symbol) === -1;
positions.push({
averagePrice: item.investment.div(item.quantity),
currency: item.currency,
firstBuyDate: item.firstBuyDate,
grossPerformance: grossPerformance[item.symbol] ?? null,
grossPerformancePercentage: holdingPeriodReturns[item.symbol]
? holdingPeriodReturns[item.symbol].minus(1)
grossPerformance: isValid
? grossPerformance[item.symbol] ?? null
: null,
grossPerformancePercentage:
isValid && holdingPeriodReturns[item.symbol]
? holdingPeriodReturns[item.symbol].minus(1)
: null,
investment: item.investment,
marketPrice: marketValue.toNumber(),
marketPrice: marketValue?.toNumber() ?? null,
name: item.name,
quantity: item.quantity,
symbol: item.symbol,
transactionCount: item.transactionCount
};
});
}
return result;
return { hasErrors, positions };
}
public async calculateTimeline(

@ -31,7 +31,7 @@ import {
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import {
HistoricalDataItem,
@ -280,12 +280,7 @@ export class PortfolioController {
@Headers('impersonation-id') impersonationId,
@Query('range') range
): Promise<PortfolioPositions> {
const positions = await this.portfolioService.getPositions(
impersonationId,
range
);
return { positions };
return await this.portfolioService.getPositions(impersonationId, range);
}
@Get('position/:symbol')

@ -397,7 +397,7 @@ export class PortfolioService {
public async getPositions(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<Position[]> {
): Promise<{ hasErrors: boolean; positions: Position[] }> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
aImpersonationId,
@ -417,23 +417,27 @@ export class PortfolioService {
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const positions = await portfolioCalculator.getCurrentPositions(startDate);
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate
);
return Object.values(positions).map((position) => {
return {
...position,
averagePrice: new Big(position.averagePrice).toNumber(),
grossPerformance: new Big(position.grossPerformance).toNumber(),
grossPerformancePercentage: new Big(
position.grossPerformancePercentage
).toNumber(),
investment: new Big(position.investment).toNumber(),
name: position.name,
quantity: new Big(position.quantity).toNumber(),
type: Type.Unknown, // TODO
url: '' // TODO
};
});
return {
hasErrors: currentPositions.hasErrors,
positions: currentPositions.positions.map((position) => {
return {
...position,
averagePrice: new Big(position.averagePrice).toNumber(),
grossPerformance: position.grossPerformance?.toNumber() ?? null,
grossPerformancePercentage:
position.grossPerformancePercentage?.toNumber() ?? null,
investment: new Big(position.investment).toNumber(),
name: position.name,
quantity: new Big(position.quantity).toNumber(),
type: Type.Unknown, // TODO
url: '' // TODO
};
})
};
}
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {

Loading…
Cancel
Save