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

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

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

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

Loading…
Cancel
Save