add error handling for current positions
This commit is contained in:
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…
x
Reference in New Issue
Block a user