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