Browse Source

Portfolio calculator rework

pull/632/head
Reto Kaul 4 years ago
parent
commit
39f8686a84
  1. 5
      apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts
  2. 438
      apps/api/src/app/portfolio/portfolio-calculator.ts
  3. 128
      apps/api/src/app/portfolio/portfolio.service.ts

5
apps/api/src/app/portfolio/interfaces/portfolio-calculator.interface.ts

@ -0,0 +1,5 @@
import { PortfolioOrder } from './portfolio-order.interface';
export interface PortfolioOrderItem extends PortfolioOrder {
itemType?: '' | 'start' | 'end';
}

438
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -7,6 +7,7 @@ import { Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
addDays, addDays,
addMilliseconds,
addMonths, addMonths,
addYears, addYears,
differenceInDays, differenceInDays,
@ -17,11 +18,12 @@ import {
max, max,
min min
} from 'date-fns'; } from 'date-fns';
import { flatten, isNumber } from 'lodash'; import { first, flatten, isNumber, sortBy } from 'lodash';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface'; import { CurrentPositions } from './interfaces/current-positions.interface';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValueObject } from './interfaces/get-value-object.interface';
import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface'; import { TimelinePeriod } from './interfaces/timeline-period.interface';
import { import {
@ -32,22 +34,34 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator { export class PortfolioCalculator {
private currency: string;
private currentRateService: CurrentRateService;
private orders: PortfolioOrder[];
private transactionPoints: TransactionPoint[]; private transactionPoints: TransactionPoint[];
public constructor( public constructor({
private currentRateService: CurrentRateService, currency,
private currency: string currentRateService,
) {} orders
}: {
currency: string;
currentRateService: CurrentRateService;
orders: PortfolioOrder[];
}) {
this.currency = currency;
this.currentRateService = currentRateService;
this.orders = orders;
public computeTransactionPoints(orders: PortfolioOrder[]) { this.orders.sort((a, b) => a.date.localeCompare(b.date));
orders.sort((a, b) => a.date.localeCompare(b.date)); }
public computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; const symbols: { [symbol: string]: TransactionPointSymbol } = {};
let lastDate: string = null; let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null; let lastTransactionPoint: TransactionPoint = null;
for (const order of orders) { for (const order of this.orders) {
const currentDate = order.date; const currentDate = order.date;
let currentTransactionPointItem: TransactionPointSymbol; let currentTransactionPointItem: TransactionPointSymbol;
@ -134,6 +148,14 @@ export class PortfolioCalculator {
} }
public async getCurrentPositions(start: Date): Promise<CurrentPositions> { public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
if (true) {
return this.getCurrentPositionsNew(start);
}
return this.getCurrentPositionsOld(start);
}
public async getCurrentPositionsOld(start: Date): Promise<CurrentPositions> {
if (!this.transactionPoints?.length) { if (!this.transactionPoints?.length) {
return { return {
currentValue: new Big(0), currentValue: new Big(0),
@ -344,6 +366,406 @@ export class PortfolioCalculator {
}; };
} }
public async getCurrentPositionsNew(start: Date): Promise<CurrentPositions> {
if (!this.transactionPoints?.length) {
return {
currentValue: new Big(0),
hasErrors: false,
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
netAnnualizedPerformance: new Big(0),
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
positions: [],
totalInvestment: new Big(0)
};
}
const lastTransactionPoint =
this.transactionPoints[this.transactionPoints.length - 1];
// use Date.now() to use the mock for today
const today = new Date(Date.now());
let firstTransactionPoint: TransactionPoint = null;
let firstIndex = this.transactionPoints.length;
const dates = [];
const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {};
dates.push(resetHours(start));
for (const item of this.transactionPoints[firstIndex - 1].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
currencies[item.symbol] = item.currency;
}
for (let i = 0; i < this.transactionPoints.length; i++) {
if (
!isBefore(parseDate(this.transactionPoints[i].date), start) &&
firstTransactionPoint === null
) {
firstTransactionPoint = this.transactionPoints[i];
firstIndex = i;
}
if (firstTransactionPoint !== null) {
dates.push(resetHours(parseDate(this.transactionPoints[i].date)));
}
}
dates.push(resetHours(today));
const marketSymbols = await this.currentRateService.getValues({
currencies,
dataGatheringItems,
dateQuery: {
in: dates
},
userCurrency: this.currency
});
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
for (const marketSymbol of marketSymbols) {
const date = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[date]) {
marketSymbolMap[date] = {};
}
if (marketSymbol.marketPrice) {
marketSymbolMap[date][marketSymbol.symbol] = new Big(
marketSymbol.marketPrice
);
}
}
const todayString = format(today, DATE_FORMAT);
if (firstIndex > 0) {
firstIndex--;
}
const initialValues: { [symbol: string]: Big } = {};
const positions: TimelinePosition[] = [];
let hasErrorsInSymbolMetrics = false;
for (const item of lastTransactionPoint.items) {
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
const {
// annualizedGrossPerformance,
// annualizedNetPerformance,
grossPerformance,
grossPerformancePercentage,
hasErrors,
initialValue,
netPerformance,
netPerformancePercentage
} = this.getSymbolMetrics({
marketSymbolMap,
start,
symbol: item.symbol
});
hasErrorsInSymbolMetrics = hasErrorsInSymbolMetrics || hasErrors;
initialValues[item.symbol] = initialValue;
positions.push({
averagePrice: item.quantity.eq(0)
? new Big(0)
: item.investment.div(item.quantity),
currency: item.currency,
dataSource: item.dataSource,
firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
grossPerformancePercentage: !hasErrors
? grossPerformancePercentage ?? null
: null,
investment: item.investment,
marketPrice: marketValue?.toNumber() ?? null,
netPerformance: !hasErrors ? netPerformance ?? null : null,
netPerformancePercentage: !hasErrors
? netPerformancePercentage ?? null
: null,
quantity: item.quantity,
symbol: item.symbol,
transactionCount: item.transactionCount
});
}
const overall = this.calculateOverallPerformance(positions, initialValues);
return {
...overall,
positions,
hasErrors: hasErrorsInSymbolMetrics || overall.hasErrors
};
}
public getSymbolMetrics({
marketSymbolMap,
start,
symbol
}: {
marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
};
start: Date;
symbol: string;
}) {
let hasErrors = false;
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
return order.symbol === symbol;
});
if (orders.length <= 0) {
return {
hasErrors,
initialValue: new Big(0),
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0)
};
}
const dateOfFirstTransaction = new Date(first(orders).date);
const endDate = new Date(Date.now());
const totalDays = new Big(
differenceInDays(
endDate,
isAfter(start, dateOfFirstTransaction) ? start : dateOfFirstTransaction
)
);
let feesAtStartDate = new Big(0);
let fees = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceAtStartDate = new Big(0);
let grossPerformanceFromSells = new Big(0);
let initialValue: Big;
let lastAveragePrice = new Big(0);
let lastValueOfInvestment = new Big(0);
let lastNetValueOfInvestment = new Big(0);
let previousOrder: PortfolioOrder = null;
let timeWeightedGrossPerformancePercentage = new Big(1);
let timeWeightedNetPerformancePercentage = new Big(1);
let totalInvestment = new Big(0);
let totalUnits = new Big(0);
const unitPriceAtStartDate =
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
const unitPriceAtEndDate =
marketSymbolMap[format(endDate, DATE_FORMAT)]?.[symbol];
if (!unitPriceAtStartDate || !unitPriceAtEndDate) {
hasErrors = true;
}
// Add a placeholder order at the start and the end date
orders.push({
symbol,
currency: null,
date: format(start, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
itemType: 'start',
name: '',
quantity: new Big(0),
type: TypeOfOrder.BUY,
unitPrice: unitPriceAtStartDate ?? new Big(0)
});
orders.push({
symbol,
currency: null,
date: format(endDate, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
itemType: 'end',
name: '',
quantity: new Big(0),
type: TypeOfOrder.BUY,
unitPrice: unitPriceAtEndDate ?? new Big(0)
});
// Sort orders so that the start and end placeholder order are at the right
// position
orders = sortBy(orders, (order) => {
let sortIndex = new Date(order.date);
if (order.itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
}
if (order.itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, 1);
}
return sortIndex.getTime();
});
const indexOfStartOrder = orders.findIndex((order) => {
return order.itemType === 'start';
});
for (let i = 0; i < orders.length; i += 1) {
const order = orders[i];
const transactionInvestment = order.quantity.mul(order.unitPrice);
if (
!initialValue &&
order.itemType !== 'start' &&
order.itemType !== 'end'
) {
initialValue = transactionInvestment;
}
fees = fees.plus(order.fee);
totalUnits = totalUnits.plus(
order.quantity.mul(this.getFactor(order.type))
);
const valueOfInvestment = totalUnits.mul(order.unitPrice);
const netValueOfInvestment = totalUnits.mul(order.unitPrice).sub(fees);
const grossPerformanceFromSell =
order.type === TypeOfOrder.SELL
? order.unitPrice.minus(lastAveragePrice).mul(order.quantity)
: new Big(0);
grossPerformanceFromSells = grossPerformanceFromSells.plus(
grossPerformanceFromSell
);
totalInvestment = totalInvestment
.plus(transactionInvestment.mul(this.getFactor(order.type)))
.plus(grossPerformanceFromSell);
lastAveragePrice = totalUnits.eq(0)
? new Big(0)
: totalInvestment.div(totalUnits);
const newGrossPerformance = valueOfInvestment
.minus(totalInvestment)
.plus(grossPerformanceFromSells);
const grossPerformanceSinceLastTransaction =
newGrossPerformance.minus(grossPerformance);
const netPerformanceSinceLastTransaction =
grossPerformanceSinceLastTransaction.minus(previousOrder?.fee ?? 0);
if (
i > indexOfStartOrder &&
!lastValueOfInvestment
.plus(transactionInvestment.mul(this.getFactor(order.type)))
.eq(0)
) {
timeWeightedGrossPerformancePercentage =
timeWeightedGrossPerformancePercentage.mul(
new Big(1).plus(
valueOfInvestment
.minus(
lastValueOfInvestment.plus(
transactionInvestment.mul(this.getFactor(order.type))
)
)
.div(
lastValueOfInvestment.plus(
transactionInvestment.mul(this.getFactor(order.type))
)
)
)
);
timeWeightedNetPerformancePercentage =
timeWeightedNetPerformancePercentage.mul(
new Big(1).plus(
netValueOfInvestment
.minus(
lastNetValueOfInvestment.plus(
transactionInvestment.mul(this.getFactor(order.type))
)
)
.div(
lastNetValueOfInvestment.plus(
transactionInvestment.mul(this.getFactor(order.type))
)
)
)
);
}
grossPerformance = newGrossPerformance;
lastNetValueOfInvestment = netValueOfInvestment;
lastValueOfInvestment = valueOfInvestment;
if (order.itemType === 'start') {
feesAtStartDate = fees;
grossPerformanceAtStartDate = grossPerformance;
}
/*console.log(`
Date: ${order.date}
Price: ${order.unitPrice}
transactionInvestment: ${transactionInvestment}
totalUnits: ${totalUnits}
totalInvestment: ${totalInvestment}
valueOfInvestment: ${valueOfInvestment}
lastAveragePrice: ${lastAveragePrice}
grossPerformanceFromSell: ${grossPerformanceFromSell}
grossPerformanceFromSells: ${grossPerformanceFromSells}
grossPerformance: ${grossPerformance.minus(grossPerformanceAtStartDate)}
netPerformance: ${grossPerformance.minus(fees)}
netPerformanceSinceLastTransaction: ${netPerformanceSinceLastTransaction}
grossPerformanceSinceLastTransaction: ${grossPerformanceSinceLastTransaction}
timeWeightedGrossPerformancePercentage: ${timeWeightedGrossPerformancePercentage}
timeWeightedNetPerformancePercentage: ${timeWeightedNetPerformancePercentage}
`);*/
previousOrder = order;
}
// console.log('\n---\n');
timeWeightedGrossPerformancePercentage =
timeWeightedGrossPerformancePercentage.sub(1);
timeWeightedNetPerformancePercentage =
timeWeightedNetPerformancePercentage.sub(1);
const totalGrossPerformance = grossPerformance.minus(
grossPerformanceAtStartDate
);
const totalNetPerformance = grossPerformance
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
return {
hasErrors,
initialValue,
annualizedNetPerformance: !totalDays.eq(0)
? totalNetPerformance.mul(totalDays).div(365)
: new Big(0),
annualizedGrossPerformance: !totalDays.eq(0)
? totalGrossPerformance.div(totalDays).mul(365)
: new Big(0),
netPerformance: totalNetPerformance,
netPerformancePercentage: timeWeightedNetPerformancePercentage,
grossPerformance: totalGrossPerformance,
grossPerformancePercentage: timeWeightedGrossPerformancePercentage
};
}
public getInvestments(): { date: string; investment: Big }[] { public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) { if (this.transactionPoints.length === 0) {
return []; return [];

128
apps/api/src/app/portfolio/portfolio.service.ts

@ -136,15 +136,18 @@ export class PortfolioService {
): Promise<InvestmentItem[]> { ): Promise<InvestmentItem[]> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator( const { portfolioOrders, transactionPoints } =
this.currentRateService, await this.getTransactionPoints({
this.request.user.Settings.currency userId,
); includeDrafts: true
});
const { transactionPoints } = await this.getTransactionPoints({ const portfolioCalculator = new PortfolioCalculator({
userId, currency: this.request.user.Settings.currency,
includeDrafts: true currentRateService: this.currentRateService,
orders: portfolioOrders
}); });
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return []; return [];
@ -185,12 +188,17 @@ export class PortfolioService {
): Promise<HistoricalDataContainer> { ): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator( const { portfolioOrders, transactionPoints } =
this.currentRateService, await this.getTransactionPoints({
this.request.user.Settings.currency userId
); });
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
const { transactionPoints } = await this.getTransactionPoints({ userId });
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return { return {
@ -272,13 +280,16 @@ export class PortfolioService {
const userId = await this.getUserId(aImpersonationId, aUserId); const userId = await this.getUserId(aImpersonationId, aUserId);
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const portfolioCalculator = new PortfolioCalculator(
this.currentRateService,
userCurrency
);
const { orders, transactionPoints } = await this.getTransactionPoints({ const { orders, portfolioOrders, transactionPoints } =
userId await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
}); });
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
@ -438,11 +449,13 @@ export class PortfolioService {
unitPrice: new Big(order.unitPrice) unitPrice: new Big(order.unitPrice)
})); }));
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator({
this.currentRateService, currency: positionCurrency,
positionCurrency currentRateService: this.currentRateService,
); orders: portfolioOrders
portfolioCalculator.computeTransactionPoints(portfolioOrders); });
portfolioCalculator.computeTransactionPoints();
const transactionPoints = portfolioCalculator.getTransactionPoints(); const transactionPoints = portfolioCalculator.getTransactionPoints();
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
@ -550,9 +563,10 @@ export class PortfolioService {
orders, orders,
transactionCount, transactionCount,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
grossPerformancePercent: position.grossPerformancePercentage.toNumber(), grossPerformancePercent:
position.grossPerformancePercentage?.toNumber(),
historicalData: historicalDataArray, historicalData: historicalDataArray,
netPerformancePercent: position.netPerformancePercentage.toNumber(), netPerformancePercent: position.netPerformancePercentage?.toNumber(),
quantity: quantity.toNumber(), quantity: quantity.toNumber(),
symbol: aSymbol, symbol: aSymbol,
value: this.exchangeRateDataService.toCurrency( value: this.exchangeRateDataService.toCurrency(
@ -629,12 +643,16 @@ export class PortfolioService {
): Promise<{ hasErrors: boolean; positions: Position[] }> { ): Promise<{ hasErrors: boolean; positions: Position[] }> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator( const { portfolioOrders, transactionPoints } =
this.currentRateService, await this.getTransactionPoints({
this.request.user.Settings.currency userId
); });
const { transactionPoints } = await this.getTransactionPoints({ userId }); const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return { return {
@ -702,12 +720,16 @@ export class PortfolioService {
): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> { ): Promise<{ hasErrors: boolean; performance: PortfolioPerformance }> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator( const { portfolioOrders, transactionPoints } =
this.currentRateService, await this.getTransactionPoints({
this.request.user.Settings.currency userId
); });
const { transactionPoints } = await this.getTransactionPoints({ userId }); const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.currency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) { if (transactionPoints?.length <= 0) {
return { return {
@ -760,9 +782,10 @@ export class PortfolioService {
const currency = this.request.user.Settings.currency; const currency = this.request.user.Settings.currency;
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
const { orders, transactionPoints } = await this.getTransactionPoints({ const { orders, portfolioOrders, transactionPoints } =
userId await this.getTransactionPoints({
}); userId
});
if (isEmpty(orders)) { if (isEmpty(orders)) {
return { return {
@ -770,10 +793,12 @@ export class PortfolioService {
}; };
} }
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator({
this.currentRateService, currency,
currency currentRateService: this.currentRateService,
); orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date); const portfolioStart = parseDate(transactionPoints[0].date);
@ -1024,6 +1049,7 @@ export class PortfolioService {
}): Promise<{ }): Promise<{
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
portfolioOrders: PortfolioOrder[];
}> { }> {
const orders = await this.orderService.getOrders({ const orders = await this.orderService.getOrders({
includeDrafts, includeDrafts,
@ -1032,7 +1058,7 @@ export class PortfolioService {
}); });
if (orders.length <= 0) { if (orders.length <= 0) {
return { transactionPoints: [], orders: [] }; return { transactionPoints: [], orders: [], portfolioOrders: [] };
} }
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
@ -1060,14 +1086,18 @@ export class PortfolioService {
) )
})); }));
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator({
this.currentRateService, currency: userCurrency,
userCurrency currentRateService: this.currentRateService,
); orders: portfolioOrders
portfolioCalculator.computeTransactionPoints(portfolioOrders); });
portfolioCalculator.computeTransactionPoints();
return { return {
transactionPoints: portfolioCalculator.getTransactionPoints(), transactionPoints: portfolioCalculator.getTransactionPoints(),
orders orders,
portfolioOrders
}; };
} }

Loading…
Cancel
Save