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/), 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). 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 ## 2.216.0 - 2025-11-10
### Changed ### Changed

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

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

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

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

Loading…
Cancel
Save