Browse Source

Bugfix/activities unexpectedly listed in holdings (#5182)

* Fix activities of type FEE, INTEREST and LIABILITY unexpectedly listed in holdings

* Update changelog
pull/5255/head^2
Attila Cseh 2 weeks ago
committed by GitHub
parent
commit
e001997ff8
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 23
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 32
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  4. 1
      apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts
  5. 8
      libs/common/src/lib/config.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
### Fixed
- Excluded the holdings originated of `FEE`, `INTEREST` and `LIABILITY` activities from the closed holdings on the portfolio holdings page
## 2.185.0 - 2025-07-26 ## 2.185.0 - 2025-07-26
### Added ### Added

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

@ -13,6 +13,7 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { import {
INVESTMENT_ACTIVITY_TYPES,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS, PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS,
PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH, PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH,
@ -287,10 +288,12 @@ export abstract class PortfolioCalculator {
firstIndex--; firstIndex--;
} }
const positions: TimelinePosition[] = []; const errors: ResponseError['errors'] = [];
let hasAnySymbolMetricsErrors = false; let hasAnySymbolMetricsErrors = false;
const errors: ResponseError['errors'] = []; const positions: (TimelinePosition & {
includeInHoldings: boolean;
})[] = [];
const accumulatedValuesByDate: { const accumulatedValuesByDate: {
[date: string]: { [date: string]: {
@ -409,6 +412,7 @@ export abstract class PortfolioCalculator {
grossPerformanceWithCurrencyEffect: !hasErrors grossPerformanceWithCurrencyEffect: !hasErrors
? (grossPerformanceWithCurrencyEffect ?? null) ? (grossPerformanceWithCurrencyEffect ?? null)
: null, : null,
includeInHoldings: item.includeInHoldings,
investment: totalInvestment, investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice: marketPrice:
@ -605,14 +609,23 @@ export abstract class PortfolioCalculator {
const overall = this.calculateOverallPerformance(positions); const overall = this.calculateOverallPerformance(positions);
const positionsIncludedInHoldings = positions
.filter(({ includeInHoldings }) => {
return includeInHoldings;
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.map(({ includeInHoldings, ...rest }) => {
return rest;
});
return { return {
...overall, ...overall,
errors, errors,
historicalData, historicalData,
positions,
totalInterestWithCurrencyEffect, totalInterestWithCurrencyEffect,
totalLiabilitiesWithCurrencyEffect, totalLiabilitiesWithCurrencyEffect,
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors,
positions: positionsIncludedInHoldings
}; };
} }
@ -935,6 +948,7 @@ export abstract class PortfolioCalculator {
dividend: new Big(0), dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee), fee: oldAccumulatedSymbol.fee.plus(fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
quantity: newQuantity, quantity: newQuantity,
tags: oldAccumulatedSymbol.tags.concat(tags), tags: oldAccumulatedSymbol.tags.concat(tags),
transactionCount: oldAccumulatedSymbol.transactionCount + 1 transactionCount: oldAccumulatedSymbol.transactionCount + 1
@ -950,6 +964,7 @@ export abstract class PortfolioCalculator {
averagePrice: unitPrice, averagePrice: unitPrice,
dividend: new Big(0), dividend: new Big(0),
firstBuyDate: date, firstBuyDate: date,
includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type),
investment: unitPrice.mul(quantity).mul(factor), investment: unitPrice.mul(quantity).mul(factor),
quantity: quantity.mul(factor), quantity: quantity.mul(factor),
transactionCount: 1 transactionCount: 1

32
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts

@ -116,37 +116,7 @@ describe('PortfolioCalculator', () => {
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: true, hasErrors: true,
positions: [ positions: [],
{
averagePrice: new Big('0'),
currency: 'USD',
dataSource: 'MANUAL',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('49'),
feeInBaseCurrency: new Big('49'),
firstBuyDate: '2021-09-01',
grossPerformance: null,
grossPerformancePercentage: null,
grossPerformancePercentageWithCurrencyEffect: null,
grossPerformanceWithCurrencyEffect: null,
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
marketPrice: null,
marketPriceInBaseCurrency: 0,
netPerformance: null,
netPerformancePercentage: null,
netPerformancePercentageWithCurrencyEffectMap: null,
netPerformanceWithCurrencyEffectMap: null,
quantity: new Big('0'),
symbol: '2c463fb3-af07-486e-adb0-8301b3d72141',
tags: [],
timeWeightedInvestment: new Big('0'),
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
transactionCount: 1,
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('49'), totalFeesWithCurrencyEffect: new Big('49'),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'), totalInvestment: new Big('0'),

1
apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts

@ -8,6 +8,7 @@ export interface TransactionPointSymbol {
dividend: Big; dividend: Big;
fee: Big; fee: Big;
firstBuyDate: string; firstBuyDate: string;
includeInHoldings: boolean;
investment: Big; investment: Big;
quantity: Big; quantity: Big;
skipErrors: boolean; skipErrors: boolean;

8
libs/common/src/lib/config.ts

@ -1,4 +1,4 @@
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource, Type } from '@prisma/client';
import { JobOptions, JobStatus } from 'bull'; import { JobOptions, JobStatus } from 'bull';
import ms from 'ms'; import ms from 'ms';
@ -118,6 +118,12 @@ export const GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_OPTIONS: JobOptions = {
removeOnComplete: true removeOnComplete: true
}; };
export const INVESTMENT_ACTIVITY_TYPES = [
Type.BUY,
Type.DIVIDEND,
Type.SELL
] as Type[];
export const PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME = 'PORTFOLIO'; export const PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME = 'PORTFOLIO';
export const PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS: JobOptions = { export const PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS: JobOptions = {
removeOnComplete: true removeOnComplete: true

Loading…
Cancel
Save