Browse Source

Handle future liabilities

pull/3118/head
Thomas Kaul 1 year ago
parent
commit
8dbce5efbd
  1. 2
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  2. 4
      apps/api/src/app/portfolio/interfaces/current-positions.interface.ts
  3. 78
      apps/api/src/app/portfolio/portfolio-calculator.ts
  4. 21
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 1
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  6. 17
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  7. 1
      libs/common/src/lib/interfaces/timeline-position.interface.ts

2
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -46,6 +46,8 @@ function mockGetValue(symbol: string, date: Date) {
case 'MSFT':
if (isSameDay(parseDate('2021-09-16'), date)) {
return { marketPrice: 89.12 };
} else if (isSameDay(parseDate('2021-11-16'), date)) {
return { marketPrice: 339.51 };
} else if (isSameDay(parseDate('2023-07-10'), date)) {
return { marketPrice: 331.83 };
}

4
apps/api/src/app/portfolio/interfaces/current-positions.interface.ts

@ -3,7 +3,7 @@ import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import Big from 'big.js';
export interface CurrentPositions extends ResponseError {
positions: TimelinePosition[];
currentValueInBaseCurrency: Big;
grossPerformance: Big;
grossPerformanceWithCurrencyEffect: Big;
grossPerformancePercentage: Big;
@ -14,6 +14,6 @@ export interface CurrentPositions extends ResponseError {
netPerformanceWithCurrencyEffect: Big;
netPerformancePercentage: Big;
netPerformancePercentageWithCurrencyEffect: Big;
currentValue: Big;
positions: TimelinePosition[];
totalInvestment: Big;
}

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

@ -449,16 +449,18 @@ export class PortfolioCalculator {
public async getCurrentPositions(
start: Date,
end = new Date(Date.now())
end?: Date
): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
const transactionPoints =
(end
? this.transactionPoints?.filter(({ date }) => {
return isBefore(parseDate(date), end);
})
: this.transactionPoints) ?? [];
if (!transactionPointsBeforeEndDate.length) {
if (!transactionPoints.length) {
return {
currentValue: new Big(0),
currentValueInBaseCurrency: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -473,41 +475,43 @@ export class PortfolioCalculator {
};
}
const lastTransactionPoint =
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
const lastTransactionPoint = last(transactionPoints);
const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
let dates: Date[] = [];
let firstIndex = transactionPointsBeforeEndDate.length;
const endDate = parseDate(lastTransactionPoint.date);
let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null;
dates.push(resetHours(start));
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
for (const { currency, dataSource, symbol } of transactionPoints[
firstIndex - 1
].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
dataSource,
symbol
});
currencies[item.symbol] = item.currency;
currencies[symbol] = currency;
}
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
for (let i = 0; i < transactionPoints.length; i++) {
if (
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
!isBefore(parseDate(transactionPoints[i].date), start) &&
firstTransactionPoint === null
) {
firstTransactionPoint = transactionPointsBeforeEndDate[i];
firstTransactionPoint = transactionPoints[i];
firstIndex = i;
}
if (firstTransactionPoint !== null) {
dates.push(
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
);
dates.push(resetHours(parseDate(transactionPoints[i].date)));
}
}
dates.push(resetHours(end));
dates.push(resetHours(endDate));
// Add dates of last week for fallback
dates.push(subDays(resetHours(new Date()), 7));
@ -534,7 +538,7 @@ export class PortfolioCalculator {
let exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end),
endDate: endOfDay(endDate),
startDate: parseDate(this.transactionPoints?.[0]?.date),
targetCurrency: this.currency
});
@ -570,7 +574,7 @@ export class PortfolioCalculator {
}
}
const endDateString = format(end, DATE_FORMAT);
const endDateString = format(endDate, DATE_FORMAT);
if (firstIndex > 0) {
firstIndex--;
@ -582,9 +586,9 @@ export class PortfolioCalculator {
const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) {
const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[
item.symbol
]?.mul(
const marketPriceInBaseCurrency = (
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
).mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
endDateString
]
@ -607,9 +611,9 @@ export class PortfolioCalculator {
totalInvestment,
totalInvestmentWithCurrencyEffect
} = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
end: endDate,
exchangeRates:
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
symbol: item.symbol
@ -656,7 +660,10 @@ export class PortfolioCalculator {
quantity: item.quantity,
symbol: item.symbol,
tags: item.tags,
transactionCount: item.transactionCount
transactionCount: item.transactionCount,
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
item.quantity
)
});
if (
@ -725,7 +732,7 @@ export class PortfolioCalculator {
}
private calculateOverallPerformance(positions: TimelinePosition[]) {
let currentValue = new Big(0);
let currentValueInBaseCurrency = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false;
@ -737,14 +744,9 @@ export class PortfolioCalculator {
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) {
if (
currentPosition.investment &&
currentPosition.marketPriceInBaseCurrency
) {
currentValue = currentValue.plus(
new Big(currentPosition.marketPriceInBaseCurrency).mul(
currentPosition.quantity
)
if (currentPosition.valueInBaseCurrency) {
currentValueInBaseCurrency = currentValueInBaseCurrency.plus(
currentPosition.valueInBaseCurrency
);
} else {
hasErrors = true;
@ -801,7 +803,7 @@ export class PortfolioCalculator {
}
return {
currentValue,
currentValueInBaseCurrency,
grossPerformance,
grossPerformanceWithCurrencyEffect,
hasErrors,

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

@ -378,7 +378,8 @@ export class PortfolioService {
});
const holdings: PortfolioDetails['holdings'] = {};
const totalValueInBaseCurrency = currentPositions.currentValue.plus(
const totalValueInBaseCurrency =
currentPositions.currentValueInBaseCurrency.plus(
cashDetails.balanceInBaseCurrency
);
@ -389,7 +390,7 @@ export class PortfolioService {
let filteredValueInBaseCurrency = isFilteredByAccount
? totalValueInBaseCurrency
: currentPositions.currentValue;
: currentPositions.currentValueInBaseCurrency;
if (
filters?.length === 0 ||
@ -444,14 +445,14 @@ export class PortfolioService {
quantity,
symbol,
tags,
transactionCount
transactionCount,
valueInBaseCurrency
} of currentPositions.positions) {
if (quantity.eq(0)) {
// Ignore positions without any quantity
continue;
}
const value = quantity.mul(marketPriceInBaseCurrency ?? 0);
const symbolProfile = symbolProfileMap[symbol];
const dataProviderResponse = dataProviderResponses[symbol];
@ -517,11 +518,11 @@ export class PortfolioService {
}
} else {
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
.plus(value)
.plus(valueInBaseCurrency)
.toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
.plus(value)
.plus(valueInBaseCurrency)
.toNumber();
}
@ -535,7 +536,7 @@ export class PortfolioService {
transactionCount,
allocationInPercentage: filteredValueInBaseCurrency.eq(0)
? 0
: value.div(filteredValueInBaseCurrency).toNumber(),
: valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(),
assetClass: symbolProfile.assetClass,
assetSubClass: symbolProfile.assetSubClass,
countries: symbolProfile.countries,
@ -560,7 +561,7 @@ export class PortfolioService {
quantity: quantity.toNumber(),
sectors: symbolProfile.sectors,
url: symbolProfile.url,
valueInBaseCurrency: value.toNumber()
valueInBaseCurrency: valueInBaseCurrency.toNumber()
};
}
@ -1175,7 +1176,7 @@ export class PortfolioService {
const startDate = this.getStartDate(dateRange, portfolioStart);
const {
currentValue,
currentValueInBaseCurrency,
errors,
grossPerformance,
grossPerformancePercentage,
@ -1270,7 +1271,7 @@ export class PortfolioService {
currentNetPerformancePercentWithCurrencyEffect.toNumber(),
currentNetPerformanceWithCurrencyEffect:
currentNetPerformanceWithCurrencyEffect.toNumber(),
currentValue: currentValue.toNumber(),
currentValue: currentValueInBaseCurrency.toNumber(),
totalInvestment: totalInvestment.toNumber()
}
};

1
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts

@ -26,6 +26,7 @@ export const ExchangeRateDataServiceMock = {
return Promise.resolve({
USDUSD: {
'2018-01-01': 1,
'2021-11-16': 1,
'2023-07-10': 1
}
});

17
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -73,7 +73,14 @@ export class ExchangeRateDataService {
currencyTo: targetCurrency
});
let previousExchangeRate = 1;
const keys = Object.keys(
exchangeRatesByCurrency[`${currency}${targetCurrency}`]
);
const lastDate = keys.reduce((a, b) => (a > b ? a : b));
let previousExchangeRate =
exchangeRatesByCurrency[`${currency}${targetCurrency}`]?.[lastDate] ??
1;
// Start from the most recent date and fill in missing exchange rates
// using the latest available rate
@ -94,7 +101,7 @@ export class ExchangeRateDataService {
exchangeRatesByCurrency[`${currency}${targetCurrency}`][dateString] =
previousExchangeRate;
if (currency === DEFAULT_CURRENCY) {
if (currency === DEFAULT_CURRENCY && isBefore(date, new Date())) {
Logger.error(
`No exchange rate has been found for ${currency}${targetCurrency} at ${dateString}`,
'ExchangeRateDataService'
@ -433,13 +440,17 @@ export class ExchangeRateDataService {
]) *
marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)];
if (isNaN(factor)) {
throw new Error('Exchange rate is not a number');
} else {
factors[format(date, DATE_FORMAT)] = factor;
}
} catch {
Logger.error(
`No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format(
date,
DATE_FORMAT
)}`,
)}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom} and ${DEFAULT_CURRENCY}${currencyTo}.`,
'ExchangeRateDataService'
);
}

1
libs/common/src/lib/interfaces/timeline-position.interface.ts

@ -27,4 +27,5 @@ export interface TimelinePosition {
timeWeightedInvestment: Big;
timeWeightedInvestmentWithCurrencyEffect: Big;
transactionCount: number;
valueInBaseCurrency: Big;
}

Loading…
Cancel
Save