Bugfix/fix gaps in chart of benchmark comparator (#4311)
* Fix benchmark interval * Update changelog
This commit is contained in:
parent
b8c6a73f30
commit
ab9133fa24
@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Improved the language localization for German (`de`)
|
- Improved the language localization for German (`de`)
|
||||||
- Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.1` to `5.2.2`
|
- Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.1` to `5.2.2`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the gaps in the chart of the benchmark comparator
|
||||||
|
|
||||||
## 2.138.0 - 2025-02-08
|
## 2.138.0 - 2025-02-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -2,7 +2,9 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
|
import { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
|
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
|
||||||
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
import type {
|
import type {
|
||||||
AssetProfileIdentifier,
|
AssetProfileIdentifier,
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
@ -16,6 +18,7 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
|
Headers,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
@ -34,6 +37,7 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
@Controller('benchmark')
|
@Controller('benchmark')
|
||||||
export class BenchmarkController {
|
export class BenchmarkController {
|
||||||
public constructor(
|
public constructor(
|
||||||
|
private readonly apiService: ApiService,
|
||||||
private readonly benchmarkService: BenchmarkService,
|
private readonly benchmarkService: BenchmarkService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
@ -108,23 +112,43 @@ export class BenchmarkController {
|
|||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
public async getBenchmarkMarketDataForUser(
|
public async getBenchmarkMarketDataForUser(
|
||||||
|
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
|
||||||
@Param('dataSource') dataSource: DataSource,
|
@Param('dataSource') dataSource: DataSource,
|
||||||
@Param('startDateString') startDateString: string,
|
@Param('startDateString') startDateString: string,
|
||||||
@Param('symbol') symbol: string,
|
@Param('symbol') symbol: string,
|
||||||
@Query('range') dateRange: DateRange = 'max'
|
@Query('range') dateRange: DateRange = 'max',
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('dataSource') filterByDataSource?: string,
|
||||||
|
@Query('symbol') filterBySymbol?: string,
|
||||||
|
@Query('tags') filterByTags?: string,
|
||||||
|
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
|
||||||
): Promise<BenchmarkMarketDataDetails> {
|
): Promise<BenchmarkMarketDataDetails> {
|
||||||
const { endDate, startDate } = getIntervalFromDateRange(
|
const { endDate, startDate } = getIntervalFromDateRange(
|
||||||
dateRange,
|
dateRange,
|
||||||
new Date(startDateString)
|
new Date(startDateString)
|
||||||
);
|
);
|
||||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
|
||||||
|
const filters = this.apiService.buildFiltersFromQueryParams({
|
||||||
|
filterByAccounts,
|
||||||
|
filterByAssetClasses,
|
||||||
|
filterByDataSource,
|
||||||
|
filterBySymbol,
|
||||||
|
filterByTags
|
||||||
|
});
|
||||||
|
|
||||||
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
|
|
||||||
return this.benchmarkService.getMarketDataForUser({
|
return this.benchmarkService.getMarketDataForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
|
dateRange,
|
||||||
endDate,
|
endDate,
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
startDate,
|
startDate,
|
||||||
symbol,
|
symbol,
|
||||||
userCurrency
|
withExcludedAccounts,
|
||||||
|
user: this.request.user
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
|
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||||
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
|
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||||
|
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
|
import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
|
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||||
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
|
||||||
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||||
|
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
|
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
|
||||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@ -19,18 +31,32 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
controllers: [BenchmarkController],
|
controllers: [BenchmarkController],
|
||||||
exports: [BenchmarkService],
|
exports: [BenchmarkService],
|
||||||
imports: [
|
imports: [
|
||||||
|
ApiModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
|
ImpersonationModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
|
OrderModule,
|
||||||
|
PortfolioSnapshotQueueModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
SymbolProfileModule,
|
SymbolProfileModule,
|
||||||
TransformDataSourceInRequestModule,
|
TransformDataSourceInRequestModule,
|
||||||
TransformDataSourceInResponseModule
|
TransformDataSourceInResponseModule,
|
||||||
|
UserModule
|
||||||
],
|
],
|
||||||
providers: [BenchmarkService]
|
providers: [
|
||||||
|
AccountBalanceService,
|
||||||
|
AccountService,
|
||||||
|
BenchmarkService,
|
||||||
|
CurrentRateService,
|
||||||
|
MarketDataService,
|
||||||
|
PortfolioCalculatorFactory,
|
||||||
|
PortfolioService,
|
||||||
|
RulesService
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class BenchmarkModule {}
|
export class BenchmarkModule {}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
|
||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
@ -22,22 +22,19 @@ import {
|
|||||||
Benchmark,
|
Benchmark,
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkProperty,
|
BenchmarkProperty,
|
||||||
BenchmarkResponse
|
BenchmarkResponse,
|
||||||
|
Filter
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
import {
|
||||||
|
BenchmarkTrend,
|
||||||
|
DateRange,
|
||||||
|
UserWithSettings
|
||||||
|
} 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 {
|
import { addHours, format, isAfter, isSameDay, subDays } from 'date-fns';
|
||||||
addHours,
|
|
||||||
differenceInDays,
|
|
||||||
eachDayOfInterval,
|
|
||||||
format,
|
|
||||||
isAfter,
|
|
||||||
isSameDay,
|
|
||||||
subDays
|
|
||||||
} from 'date-fns';
|
|
||||||
import { isNumber, uniqBy } from 'lodash';
|
import { isNumber, uniqBy } from 'lodash';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
@ -48,11 +45,11 @@ export class BenchmarkService {
|
|||||||
private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS';
|
private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS';
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly portfolioService: PortfolioService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
private readonly symbolProfileService: SymbolProfileService,
|
private readonly symbolProfileService: SymbolProfileService,
|
||||||
@ -158,31 +155,33 @@ export class BenchmarkService {
|
|||||||
|
|
||||||
public async getMarketDataForUser({
|
public async getMarketDataForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
|
dateRange,
|
||||||
endDate = new Date(),
|
endDate = new Date(),
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
startDate,
|
startDate,
|
||||||
symbol,
|
symbol,
|
||||||
userCurrency
|
user,
|
||||||
|
withExcludedAccounts
|
||||||
}: {
|
}: {
|
||||||
|
dateRange: DateRange;
|
||||||
endDate?: Date;
|
endDate?: Date;
|
||||||
|
filters?: Filter[];
|
||||||
|
impersonationId: string;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
userCurrency: string;
|
user: UserWithSettings;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
} & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
|
} & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
|
||||||
const marketData: { date: string; value: number }[] = [];
|
const marketData: { date: string; value: number }[] = [];
|
||||||
|
const userCurrency = user.Settings.settings.baseCurrency;
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
const days = differenceInDays(endDate, startDate) + 1;
|
const { chart } = await this.portfolioService.getPerformance({
|
||||||
const dates = eachDayOfInterval(
|
dateRange,
|
||||||
{
|
filters,
|
||||||
start: startDate,
|
impersonationId,
|
||||||
end: endDate
|
userId,
|
||||||
},
|
withExcludedAccounts
|
||||||
{
|
|
||||||
step: Math.round(
|
|
||||||
days /
|
|
||||||
Math.min(days, this.configurationService.get('MAX_CHART_ITEMS'))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
).map((date) => {
|
|
||||||
return resetHours(date);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentSymbolItem, marketDataItems] = await Promise.all([
|
const [currentSymbolItem, marketDataItems] = await Promise.all([
|
||||||
@ -200,7 +199,9 @@ export class BenchmarkService {
|
|||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
date: {
|
date: {
|
||||||
in: dates
|
in: chart.map(({ date }) => {
|
||||||
|
return resetHours(parseDate(date));
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -321,6 +321,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
.fetchBenchmarkForUser({
|
.fetchBenchmarkForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol,
|
symbol,
|
||||||
|
filters: this.userService.getFilters(),
|
||||||
range: this.user?.settings?.dateRange,
|
range: this.user?.settings?.dateRange,
|
||||||
startDate: this.firstOrderDate
|
startDate: this.firstOrderDate
|
||||||
})
|
})
|
||||||
|
@ -338,17 +338,23 @@ export class DataService {
|
|||||||
|
|
||||||
public fetchBenchmarkForUser({
|
public fetchBenchmarkForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
|
filters,
|
||||||
range,
|
range,
|
||||||
startDate,
|
startDate,
|
||||||
symbol
|
symbol,
|
||||||
|
withExcludedAccounts
|
||||||
}: {
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
range: DateRange;
|
range: DateRange;
|
||||||
startDate: Date;
|
startDate: Date;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
} & AssetProfileIdentifier): Observable<BenchmarkMarketDataDetails> {
|
} & AssetProfileIdentifier): Observable<BenchmarkMarketDataDetails> {
|
||||||
let params = new HttpParams();
|
let params = this.buildFiltersAsQueryParams({ filters });
|
||||||
|
|
||||||
if (range) {
|
params = params.append('range', range);
|
||||||
params = params.append('range', range);
|
|
||||||
|
if (withExcludedAccounts) {
|
||||||
|
params = params.append('withExcludedAccounts', withExcludedAccounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<BenchmarkMarketDataDetails>(
|
return this.http.get<BenchmarkMarketDataDetails>(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user