Browse Source

Merge branch 'main' into bugfix/skip-opening-holding-detail-dialog-for-cash-positions

pull/7390/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
de862cf2ee
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 4
      apps/api/src/app/access/access.controller.ts
  3. 6
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  4. 65
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 4
      apps/api/src/services/benchmark/benchmark.service.ts
  6. 4
      apps/client/src/app/components/investment-chart/investment-chart.component.ts

2
CHANGELOG.md

@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Skipped opening the holding detail dialog for cash positions on the allocations page, the analysis page and the portfolio holdings page - Skipped opening the holding detail dialog for cash positions on the allocations page, the analysis page and the portfolio holdings page
- Resolved an exception in the `GET api/v1/portfolio/holding/:dataSource/:symbol` endpoint for cash positions
- Improved the error handling in the access endpoints (`POST` and `PUT`) to return `400 Bad Request` when granting access to a non-existent user
## 3.31.0 - 2026-07-20 ## 3.31.0 - 2026-07-20

4
apps/api/src/app/access/access.controller.ts

@ -87,7 +87,7 @@ export class AccessController {
} }
try { try {
return this.accessService.createAccess({ return await this.accessService.createAccess({
alias: data.alias || undefined, alias: data.alias || undefined,
granteeUser: data.granteeUserId granteeUser: data.granteeUserId
? { connect: { id: data.granteeUserId } } ? { connect: { id: data.granteeUserId } }
@ -155,7 +155,7 @@ export class AccessController {
} }
try { try {
return this.accessService.updateAccess({ return await this.accessService.updateAccess({
data: { data: {
alias: data.alias, alias: data.alias,
granteeUser: data.granteeUserId granteeUser: data.granteeUserId

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

@ -51,6 +51,8 @@ import {
format, format,
isAfter, isAfter,
isBefore, isBefore,
isFuture,
isPast,
isWithinInterval, isWithinInterval,
min, min,
startOfDay, startOfDay,
@ -134,7 +136,7 @@ export abstract class PortfolioCalculator {
dateOfFirstActivity = date; dateOfFirstActivity = date;
} }
if (isAfter(date, new Date())) { if (isFuture(date)) {
// Adapt date to today if activity is in future (e.g. liability) // Adapt date to today if activity is in future (e.g. liability)
// to include it in the interval // to include it in the interval
date = endOfDay(new Date()); date = endOfDay(new Date());
@ -1113,7 +1115,7 @@ export abstract class PortfolioCalculator {
portfolioSnapshot portfolioSnapshot
); );
if (isAfter(new Date(), new Date(expiration))) { if (isPast(new Date(expiration))) {
isCachedPortfolioSnapshotExpired = true; isCachedPortfolioSnapshotExpired = true;
} }
} catch {} } catch {}

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

@ -783,10 +783,24 @@ export class PortfolioService {
return undefined; return undefined;
} }
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
{ dataSource, symbol } { dataSource, symbol }
]); ]);
const assetProfile =
symbolProfile ??
({
dataSource,
symbol,
assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH,
countries: [],
currency: symbol,
holdings: [],
name: symbol,
sectors: []
} as EnhancedSymbolProfile);
const portfolioCalculator = this.calculatorFactory.createCalculator({ const portfolioCalculator = this.calculatorFactory.createCalculator({
activities, activities,
userId, userId,
@ -829,9 +843,10 @@ export class PortfolioService {
timeWeightedInvestmentWithCurrencyEffect timeWeightedInvestmentWithCurrencyEffect
} = holding; } = holding;
const activitiesOfHolding = activities.filter(({ assetProfile }) => { const activitiesOfHolding = activities.filter((activity) => {
return ( return (
assetProfile.dataSource === dataSource && assetProfile.symbol === symbol activity.assetProfile.dataSource === dataSource &&
activity.assetProfile.symbol === symbol
); );
}); });
@ -863,19 +878,17 @@ export class PortfolioService {
new Date() new Date()
); );
const [firstActivity] = activitiesOfHolding;
const referenceUnitPrice =
firstActivity?.unitPriceInAssetProfileCurrency ?? marketPrice;
const historicalDataArray: HistoricalDataItem[] = []; const historicalDataArray: HistoricalDataItem[] = [];
let marketPriceMax = Math.max( let marketPriceMax = Math.max(referenceUnitPrice, marketPrice);
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice
);
let marketPriceMaxDate = let marketPriceMaxDate =
marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency marketPrice > referenceUnitPrice
? new Date() ? new Date()
: activitiesOfHolding[0].date; : (firstActivity?.date ?? new Date());
let marketPriceMin = Math.min( let marketPriceMin = Math.min(referenceUnitPrice, marketPrice);
activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
marketPrice
);
const historicalDataItems = const historicalDataItems =
historicalData[getAssetProfileIdentifier({ dataSource, symbol })]; historicalData[getAssetProfileIdentifier({ dataSource, symbol })];
@ -926,10 +939,10 @@ export class PortfolioService {
} else { } else {
// Add historical entry for buy date, if no historical data available // Add historical entry for buy date, if no historical data available
historicalDataArray.push({ historicalDataArray.push({
averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, averagePrice: referenceUnitPrice,
date: dateOfFirstActivity, date: dateOfFirstActivity,
marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, marketPrice: referenceUnitPrice,
quantity: activitiesOfHolding[0].quantity quantity: firstActivity?.quantity ?? quantity.toNumber()
}); });
} }
@ -947,16 +960,16 @@ export class PortfolioService {
marketPriceMin, marketPriceMin,
tags, tags,
assetProfile: { assetProfile: {
assetClass: SymbolProfile.assetClass, assetClass: assetProfile.assetClass,
assetSubClass: SymbolProfile.assetSubClass, assetSubClass: assetProfile.assetSubClass,
countries: SymbolProfile.countries, countries: assetProfile.countries,
currency: SymbolProfile.currency, currency: assetProfile.currency,
dataSource: SymbolProfile.dataSource, dataSource: assetProfile.dataSource,
isin: SymbolProfile.isin, isin: assetProfile.isin,
name: SymbolProfile.name, name: assetProfile.name,
sectors: SymbolProfile.sectors, sectors: assetProfile.sectors,
symbol: SymbolProfile.symbol, symbol: assetProfile.symbol,
userId: SymbolProfile.userId userId: assetProfile.userId
}, },
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],

4
apps/api/src/services/benchmark/benchmark.service.ts

@ -23,7 +23,7 @@ import { BenchmarkTrend } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { addHours, isAfter, subDays } from 'date-fns'; import { addHours, isPast, subDays } from 'date-fns';
import { round, uniqBy } from 'lodash'; import { round, uniqBy } from 'lodash';
import ms from 'ms'; import ms from 'ms';
@ -94,7 +94,7 @@ export class BenchmarkService {
this.logger.debug('Fetched benchmarks from cache'); this.logger.debug('Fetched benchmarks from cache');
if (isAfter(new Date(), new Date(expiration))) { if (isPast(new Date(expiration))) {
this.calculateAndCacheBenchmarks({ this.calculateAndCacheBenchmarks({
enableSharing enableSharing
}); });

4
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -41,7 +41,7 @@ import {
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { type AnnotationOptions } from 'chartjs-plugin-annotation'; import { type AnnotationOptions } from 'chartjs-plugin-annotation';
import { isAfter } from 'date-fns'; import { isFuture } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@Component({ @Component({
@ -311,6 +311,6 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
return undefined; return undefined;
} }
return isAfter(new Date(xValue), new Date()) ? aValue : undefined; return isFuture(new Date(xValue)) ? aValue : undefined;
} }
} }

Loading…
Cancel
Save