Browse Source

Task/refactor getHolding() in portfolio service (#5898)

* Refactor getHolding() if no holding has been found

* Update changelog
pull/5931/head
Thomas Kaul 3 weeks ago
committed by GitHub
parent
commit
9f878c42f4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 19
      apps/api/src/app/import/import.service.ts
  3. 421
      apps/api/src/app/portfolio/portfolio.service.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Refactored the get holding functionality in the portfolio service
## 2.216.0 - 2025-11-10
### Changed

19
apps/api/src/app/import/import.service.ts

@ -58,13 +58,18 @@ export class ImportService {
userId
}: AssetProfileIdentifier & { userId: string }): Promise<Activity[]> {
try {
const { activities, firstBuyDate, historicalData } =
await this.portfolioService.getHolding({
dataSource,
symbol,
userId,
impersonationId: undefined
});
const holding = await this.portfolioService.getHolding({
dataSource,
symbol,
userId,
impersonationId: undefined
});
if (!holding) {
return [];
}
const { activities, firstBuyDate, historicalData } = holding;
const [[assetProfile], dividends] = await Promise.all([
this.symbolProfileService.getSymbolProfiles([

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

@ -88,7 +88,6 @@ import {
parseISO,
set
} from 'date-fns';
import { isEmpty } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator';
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
@ -776,35 +775,7 @@ export class PortfolioService {
});
if (activities.length === 0) {
return {
activities: [],
activitiesCount: 0,
averagePrice: undefined,
dataProviderInfo: undefined,
dividendInBaseCurrency: undefined,
dividendYieldPercent: undefined,
dividendYieldPercentWithCurrencyEffect: undefined,
feeInBaseCurrency: undefined,
firstBuyDate: undefined,
grossPerformance: undefined,
grossPerformancePercent: undefined,
grossPerformancePercentWithCurrencyEffect: undefined,
grossPerformanceWithCurrencyEffect: undefined,
historicalData: [],
investmentInBaseCurrencyWithCurrencyEffect: undefined,
marketPrice: undefined,
marketPriceMax: undefined,
marketPriceMin: undefined,
netPerformance: undefined,
netPerformancePercent: undefined,
netPerformancePercentWithCurrencyEffect: undefined,
netPerformanceWithCurrencyEffect: undefined,
performances: undefined,
quantity: undefined,
SymbolProfile: undefined,
tags: [],
value: undefined
};
return undefined;
}
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
@ -818,7 +789,6 @@ export class PortfolioService {
currency: userCurrency
});
const portfolioStart = portfolioCalculator.getStartDate();
const transactionPoints = portfolioCalculator.getTransactionPoints();
const { positions } = await portfolioCalculator.getSnapshot();
@ -827,225 +797,108 @@ export class PortfolioService {
return position.dataSource === dataSource && position.symbol === symbol;
});
if (holding) {
const {
averagePrice,
currency,
dividendInBaseCurrency,
fee,
firstBuyDate,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
investmentWithCurrencyEffect,
marketPrice,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffectMap,
netPerformanceWithCurrencyEffectMap,
quantity,
tags,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
transactionCount
} = holding;
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === dataSource &&
SymbolProfile.symbol === symbol
);
});
const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercentage: timeWeightedInvestment.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestment)
});
const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(
0
)
? new Big(0)
: dividendInBaseCurrency.div(
timeWeightedInvestmentWithCurrencyEffect
)
});
if (!holding) {
return undefined;
}
const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol }],
'day',
parseISO(firstBuyDate),
new Date()
);
const {
averagePrice,
currency,
dividendInBaseCurrency,
fee,
firstBuyDate,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
investmentWithCurrencyEffect,
marketPrice,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffectMap,
netPerformanceWithCurrencyEffectMap,
quantity,
tags,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
transactionCount
} = holding;
const historicalDataArray: HistoricalDataItem[] = [];
let marketPriceMax = Math.max(
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice
);
let marketPriceMaxDate =
marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency
? new Date()
: activitiesOfHolding[0].date;
let marketPriceMin = Math.min(
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === dataSource &&
SymbolProfile.symbol === symbol
);
});
if (historicalData[symbol]) {
let j = -1;
for (const [date, { marketPrice }] of Object.entries(
historicalData[symbol]
)) {
while (
j + 1 < transactionPoints.length &&
!isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date))
) {
j++;
}
let currentAveragePrice = 0;
let currentQuantity = 0;
const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercentage: timeWeightedInvestment.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestment)
});
const currentSymbol = transactionPoints[j]?.items.find(
(transactionPointSymbol) => {
return transactionPointSymbol.symbol === symbol;
}
);
const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)),
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestmentWithCurrencyEffect)
});
if (currentSymbol) {
currentAveragePrice = currentSymbol.averagePrice.toNumber();
currentQuantity = currentSymbol.quantity.toNumber();
}
const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol }],
'day',
parseISO(firstBuyDate),
new Date()
);
historicalDataArray.push({
date,
averagePrice: currentAveragePrice,
marketPrice:
historicalDataArray.length > 0
? marketPrice
: currentAveragePrice,
quantity: currentQuantity
});
const historicalDataArray: HistoricalDataItem[] = [];
let marketPriceMax = Math.max(
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice
);
let marketPriceMaxDate =
marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency
? new Date()
: activitiesOfHolding[0].date;
let marketPriceMin = Math.min(
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice
);
if (marketPrice > marketPriceMax) {
marketPriceMax = marketPrice;
marketPriceMaxDate = parseISO(date);
}
marketPriceMin = Math.min(
marketPrice ?? Number.MAX_SAFE_INTEGER,
marketPriceMin
);
if (historicalData[symbol]) {
let j = -1;
for (const [date, { marketPrice }] of Object.entries(
historicalData[symbol]
)) {
while (
j + 1 < transactionPoints.length &&
!isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date))
) {
j++;
}
} else {
// Add historical entry for buy date, if no historical data available
historicalDataArray.push({
averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
date: firstBuyDate,
marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
quantity: activitiesOfHolding[0].quantity
});
}
const performancePercent =
this.benchmarkService.calculateChangeInPercentage(
marketPriceMax,
marketPrice
);
let currentAveragePrice = 0;
let currentQuantity = 0;
return {
firstBuyDate,
marketPrice,
marketPriceMax,
marketPriceMin,
SymbolProfile,
tags,
activities: activitiesOfHolding,
activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
dividendYieldPercent: dividendYieldPercent.toNumber(),
dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(),
SymbolProfile.currency,
userCurrency
),
grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(),
grossPerformancePercentWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect?.toNumber(),
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect?.toNumber(),
historicalData: historicalDataArray,
investmentInBaseCurrencyWithCurrencyEffect:
investmentWithCurrencyEffect?.toNumber(),
netPerformance: netPerformance?.toNumber(),
netPerformancePercent: netPerformancePercentage?.toNumber(),
netPerformancePercentWithCurrencyEffect:
netPerformancePercentageWithCurrencyEffectMap?.['max']?.toNumber(),
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(),
performances: {
allTimeHigh: {
performancePercent,
date: marketPriceMaxDate
const currentSymbol = transactionPoints[j]?.items.find(
(transactionPointSymbol) => {
return transactionPointSymbol.symbol === symbol;
}
},
quantity: quantity.toNumber(),
value: this.exchangeRateDataService.toCurrency(
quantity.mul(marketPrice ?? 0).toNumber(),
currency,
userCurrency
)
};
} else {
const currentData = await this.dataProviderService.getQuotes({
user,
items: [{ symbol, dataSource: DataSource.YAHOO }]
});
const marketPrice = currentData[symbol]?.marketPrice;
let historicalData = await this.dataProviderService.getHistorical(
[{ symbol, dataSource: DataSource.YAHOO }],
'day',
portfolioStart,
new Date()
);
);
if (isEmpty(historicalData)) {
try {
historicalData = await this.dataProviderService.getHistoricalRaw({
assetProfileIdentifiers: [{ symbol, dataSource: DataSource.YAHOO }],
from: portfolioStart,
to: new Date()
});
} catch {
historicalData = {
[symbol]: {}
};
if (currentSymbol) {
currentAveragePrice = currentSymbol.averagePrice.toNumber();
currentQuantity = currentSymbol.quantity.toNumber();
}
}
const historicalDataArray: HistoricalDataItem[] = [];
let marketPriceMax = marketPrice;
let marketPriceMaxDate = new Date();
let marketPriceMin = marketPrice;
for (const [date, { marketPrice }] of Object.entries(
historicalData[symbol]
)) {
historicalDataArray.push({
date,
value: marketPrice
averagePrice: currentAveragePrice,
marketPrice:
historicalDataArray.length > 0 ? marketPrice : currentAveragePrice,
quantity: currentQuantity
});
if (marketPrice > marketPriceMax) {
@ -1057,48 +910,70 @@ export class PortfolioService {
marketPriceMin
);
}
} else {
// Add historical entry for buy date, if no historical data available
historicalDataArray.push({
averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
date: firstBuyDate,
marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
quantity: activitiesOfHolding[0].quantity
});
}
const performancePercent =
this.benchmarkService.calculateChangeInPercentage(
marketPriceMax,
marketPrice
);
return {
marketPrice,
const performancePercent =
this.benchmarkService.calculateChangeInPercentage(
marketPriceMax,
marketPriceMin,
SymbolProfile,
activities: [],
activitiesCount: 0,
averagePrice: 0,
dataProviderInfo: undefined,
dividendInBaseCurrency: 0,
dividendYieldPercent: 0,
dividendYieldPercentWithCurrencyEffect: 0,
feeInBaseCurrency: 0,
firstBuyDate: undefined,
grossPerformance: undefined,
grossPerformancePercent: undefined,
grossPerformancePercentWithCurrencyEffect: undefined,
grossPerformanceWithCurrencyEffect: undefined,
historicalData: historicalDataArray,
investmentInBaseCurrencyWithCurrencyEffect: 0,
netPerformance: undefined,
netPerformancePercent: undefined,
netPerformancePercentWithCurrencyEffect: undefined,
netPerformanceWithCurrencyEffect: undefined,
performances: {
allTimeHigh: {
performancePercent,
date: marketPriceMaxDate
}
},
quantity: 0,
tags: [],
value: 0
};
}
marketPrice
);
return {
firstBuyDate,
marketPrice,
marketPriceMax,
marketPriceMin,
SymbolProfile,
tags,
activities: activitiesOfHolding,
activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
dividendYieldPercent: dividendYieldPercent.toNumber(),
dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(),
SymbolProfile.currency,
userCurrency
),
grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(),
grossPerformancePercentWithCurrencyEffect:
grossPerformancePercentageWithCurrencyEffect?.toNumber(),
grossPerformanceWithCurrencyEffect:
grossPerformanceWithCurrencyEffect?.toNumber(),
historicalData: historicalDataArray,
investmentInBaseCurrencyWithCurrencyEffect:
investmentWithCurrencyEffect?.toNumber(),
netPerformance: netPerformance?.toNumber(),
netPerformancePercent: netPerformancePercentage?.toNumber(),
netPerformancePercentWithCurrencyEffect:
netPerformancePercentageWithCurrencyEffectMap?.['max']?.toNumber(),
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(),
performances: {
allTimeHigh: {
performancePercent,
date: marketPriceMaxDate
}
},
quantity: quantity.toNumber(),
value: this.exchangeRateDataService.toCurrency(
quantity.mul(marketPrice ?? 0).toNumber(),
currency,
userCurrency
)
};
}
public async getPerformance({

Loading…
Cancel
Save