Bugfix/cannot register cron jobs (#4325)
* Reorganize benchmark modules and move benchmarks endpoint
This commit is contained in:
parent
239adc1045
commit
e26b015407
@ -1,8 +1,8 @@
|
|||||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
|
||||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||||
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
|
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.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 { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
|
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
|
||||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||||
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
|
||||||
import { environment } from '@ghostfolio/api/environments/environment';
|
import { environment } from '@ghostfolio/api/environments/environment';
|
||||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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';
|
||||||
|
@ -29,10 +29,10 @@ import { AppController } from './app.controller';
|
|||||||
import { AssetModule } from './asset/asset.module';
|
import { AssetModule } from './asset/asset.module';
|
||||||
import { AuthDeviceModule } from './auth-device/auth-device.module';
|
import { AuthDeviceModule } from './auth-device/auth-device.module';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
|
||||||
import { CacheModule } from './cache/cache.module';
|
import { CacheModule } from './cache/cache.module';
|
||||||
import { AiModule } from './endpoints/ai/ai.module';
|
import { AiModule } from './endpoints/ai/ai.module';
|
||||||
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
|
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
|
||||||
|
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
|
||||||
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
|
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
|
||||||
import { MarketDataModule } from './endpoints/market-data/market-data.module';
|
import { MarketDataModule } from './endpoints/market-data/market-data.module';
|
||||||
import { PublicModule } from './endpoints/public/public.module';
|
import { PublicModule } from './endpoints/public/public.module';
|
||||||
@ -63,7 +63,7 @@ import { UserModule } from './user/user.module';
|
|||||||
AssetModule,
|
AssetModule,
|
||||||
AuthDeviceModule,
|
AuthDeviceModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
BenchmarkModule,
|
BenchmarksModule,
|
||||||
BullModule.forRoot({
|
BullModule.forRoot({
|
||||||
redis: {
|
redis: {
|
||||||
db: parseInt(process.env.REDIS_DB ?? '0', 10),
|
db: parseInt(process.env.REDIS_DB ?? '0', 10),
|
||||||
|
@ -3,6 +3,7 @@ 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 { ApiService } from '@ghostfolio/api/services/api/api.service';
|
||||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
|
||||||
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
|
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
|
||||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||||
import type {
|
import type {
|
||||||
@ -32,13 +33,14 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { BenchmarkService } from './benchmark.service';
|
import { BenchmarksService } from './benchmarks.service';
|
||||||
|
|
||||||
@Controller('benchmark')
|
@Controller('benchmarks')
|
||||||
export class BenchmarkController {
|
export class BenchmarksController {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
private readonly benchmarkService: BenchmarkService,
|
private readonly benchmarkService: BenchmarkService,
|
||||||
|
private readonly benchmarksService: BenchmarksService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -139,7 +141,7 @@ export class BenchmarkController {
|
|||||||
|
|
||||||
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
const withExcludedAccounts = withExcludedAccountsParam === 'true';
|
||||||
|
|
||||||
return this.benchmarkService.getMarketDataForUser({
|
return this.benchmarksService.getMarketDataForUser({
|
||||||
dataSource,
|
dataSource,
|
||||||
dateRange,
|
dateRange,
|
||||||
endDate,
|
endDate,
|
@ -11,6 +11,7 @@ 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 { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
|
||||||
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';
|
||||||
@ -24,12 +25,11 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/sym
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { BenchmarkController } from './benchmark.controller';
|
import { BenchmarksController } from './benchmarks.controller';
|
||||||
import { BenchmarkService } from './benchmark.service';
|
import { BenchmarksService } from './benchmarks.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [BenchmarkController],
|
controllers: [BenchmarksController],
|
||||||
exports: [BenchmarkService],
|
|
||||||
imports: [
|
imports: [
|
||||||
ApiModule,
|
ApiModule,
|
||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
@ -52,6 +52,7 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
AccountBalanceService,
|
AccountBalanceService,
|
||||||
AccountService,
|
AccountService,
|
||||||
BenchmarkService,
|
BenchmarkService,
|
||||||
|
BenchmarksService,
|
||||||
CurrentRateService,
|
CurrentRateService,
|
||||||
MarketDataService,
|
MarketDataService,
|
||||||
PortfolioCalculatorFactory,
|
PortfolioCalculatorFactory,
|
||||||
@ -59,4 +60,4 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
RulesService
|
RulesService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class BenchmarkModule {}
|
export class BenchmarksModule {}
|
163
apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
Normal file
163
apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||||
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.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 { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
|
import {
|
||||||
|
AssetProfileIdentifier,
|
||||||
|
BenchmarkMarketDataDetails,
|
||||||
|
Filter
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { DateRange, UserWithSettings } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { format, isSameDay } from 'date-fns';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BenchmarksService {
|
||||||
|
public constructor(
|
||||||
|
private readonly benchmarkService: BenchmarkService,
|
||||||
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
|
private readonly marketDataService: MarketDataService,
|
||||||
|
private readonly portfolioService: PortfolioService,
|
||||||
|
private readonly symbolService: SymbolService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async getMarketDataForUser({
|
||||||
|
dataSource,
|
||||||
|
dateRange,
|
||||||
|
endDate = new Date(),
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
|
startDate,
|
||||||
|
symbol,
|
||||||
|
user,
|
||||||
|
withExcludedAccounts
|
||||||
|
}: {
|
||||||
|
dateRange: DateRange;
|
||||||
|
endDate?: Date;
|
||||||
|
filters?: Filter[];
|
||||||
|
impersonationId: string;
|
||||||
|
startDate: Date;
|
||||||
|
user: UserWithSettings;
|
||||||
|
withExcludedAccounts?: boolean;
|
||||||
|
} & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
|
||||||
|
const marketData: { date: string; value: number }[] = [];
|
||||||
|
const userCurrency = user.Settings.settings.baseCurrency;
|
||||||
|
const userId = user.id;
|
||||||
|
|
||||||
|
const { chart } = await this.portfolioService.getPerformance({
|
||||||
|
dateRange,
|
||||||
|
filters,
|
||||||
|
impersonationId,
|
||||||
|
userId,
|
||||||
|
withExcludedAccounts
|
||||||
|
});
|
||||||
|
|
||||||
|
const [currentSymbolItem, marketDataItems] = await Promise.all([
|
||||||
|
this.symbolService.get({
|
||||||
|
dataGatheringItem: {
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.marketDataService.marketDataItems({
|
||||||
|
orderBy: {
|
||||||
|
date: 'asc'
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
date: {
|
||||||
|
in: chart.map(({ date }) => {
|
||||||
|
return resetHours(parseDate(date));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exchangeRates =
|
||||||
|
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
||||||
|
startDate,
|
||||||
|
currencies: [currentSymbolItem.currency],
|
||||||
|
targetCurrency: userCurrency
|
||||||
|
});
|
||||||
|
|
||||||
|
const exchangeRateAtStartDate =
|
||||||
|
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
|
||||||
|
format(startDate, DATE_FORMAT)
|
||||||
|
];
|
||||||
|
|
||||||
|
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
|
||||||
|
return isSameDay(date, startDate);
|
||||||
|
})?.marketPrice;
|
||||||
|
|
||||||
|
if (!marketPriceAtStartDate) {
|
||||||
|
Logger.error(
|
||||||
|
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
|
||||||
|
startDate,
|
||||||
|
DATE_FORMAT
|
||||||
|
)}`,
|
||||||
|
'BenchmarkService'
|
||||||
|
);
|
||||||
|
|
||||||
|
return { marketData };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const marketDataItem of marketDataItems) {
|
||||||
|
const exchangeRate =
|
||||||
|
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
|
||||||
|
format(marketDataItem.date, DATE_FORMAT)
|
||||||
|
];
|
||||||
|
|
||||||
|
const exchangeRateFactor =
|
||||||
|
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
|
||||||
|
? exchangeRate / exchangeRateAtStartDate
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
marketData.push({
|
||||||
|
date: format(marketDataItem.date, DATE_FORMAT),
|
||||||
|
value:
|
||||||
|
marketPriceAtStartDate === 0
|
||||||
|
? 0
|
||||||
|
: this.benchmarkService.calculateChangeInPercentage(
|
||||||
|
marketPriceAtStartDate,
|
||||||
|
marketDataItem.marketPrice * exchangeRateFactor
|
||||||
|
) * 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const includesEndDate = isSameDay(
|
||||||
|
parseDate(marketData.at(-1).date),
|
||||||
|
endDate
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentSymbolItem?.marketPrice && !includesEndDate) {
|
||||||
|
const exchangeRate =
|
||||||
|
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
|
||||||
|
format(endDate, DATE_FORMAT)
|
||||||
|
];
|
||||||
|
|
||||||
|
const exchangeRateFactor =
|
||||||
|
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
|
||||||
|
? exchangeRate / exchangeRateAtStartDate
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
marketData.push({
|
||||||
|
date: format(endDate, DATE_FORMAT),
|
||||||
|
value:
|
||||||
|
this.benchmarkService.calculateChangeInPercentage(
|
||||||
|
marketPriceAtStartDate,
|
||||||
|
currentSymbolItem.marketPrice * exchangeRateFactor
|
||||||
|
) * 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
marketData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
|
||||||
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
import { PlatformModule } from '@ghostfolio/api/app/platform/platform.module';
|
||||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
import { UserModule } from '@ghostfolio/api/app/user/user.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 { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
|
||||||
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
|
||||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
24
apps/api/src/services/benchmark/benchmark.module.ts
Normal file
24
apps/api/src/services/benchmark/benchmark.module.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||||
|
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||||
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [BenchmarkService],
|
||||||
|
imports: [
|
||||||
|
DataProviderModule,
|
||||||
|
MarketDataModule,
|
||||||
|
PrismaModule,
|
||||||
|
PropertyModule,
|
||||||
|
RedisCacheModule,
|
||||||
|
SymbolProfileModule
|
||||||
|
],
|
||||||
|
providers: [BenchmarkService]
|
||||||
|
})
|
||||||
|
export class BenchmarkModule {}
|
@ -4,17 +4,7 @@ describe('BenchmarkService', () => {
|
|||||||
let benchmarkService: BenchmarkService;
|
let benchmarkService: BenchmarkService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
benchmarkService = new BenchmarkService(
|
benchmarkService = new BenchmarkService(null, null, null, null, null, null);
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculateChangeInPercentage', async () => {
|
it('calculateChangeInPercentage', async () => {
|
@ -1,8 +1,5 @@
|
|||||||
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 { 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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
@ -11,31 +8,20 @@ import {
|
|||||||
CACHE_TTL_INFINITE,
|
CACHE_TTL_INFINITE,
|
||||||
PROPERTY_BENCHMARKS
|
PROPERTY_BENCHMARKS
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import {
|
import { calculateBenchmarkTrend } from '@ghostfolio/common/helper';
|
||||||
DATE_FORMAT,
|
|
||||||
calculateBenchmarkTrend,
|
|
||||||
parseDate,
|
|
||||||
resetHours
|
|
||||||
} from '@ghostfolio/common/helper';
|
|
||||||
import {
|
import {
|
||||||
AssetProfileIdentifier,
|
AssetProfileIdentifier,
|
||||||
Benchmark,
|
Benchmark,
|
||||||
BenchmarkMarketDataDetails,
|
|
||||||
BenchmarkProperty,
|
BenchmarkProperty,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse
|
||||||
Filter
|
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import { BenchmarkTrend } from '@ghostfolio/common/types';
|
||||||
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 { addHours, format, isAfter, isSameDay, subDays } from 'date-fns';
|
import { addHours, isAfter, subDays } from 'date-fns';
|
||||||
import { isNumber, uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
import { BenchmarkValue } from './interfaces/benchmark-value.interface';
|
import { BenchmarkValue } from './interfaces/benchmark-value.interface';
|
||||||
@ -46,14 +32,11 @@ export class BenchmarkService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
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
|
||||||
private readonly symbolService: SymbolService
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
|
public calculateChangeInPercentage(baseValue: number, currentValue: number) {
|
||||||
@ -153,142 +136,6 @@ export class BenchmarkService {
|
|||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMarketDataForUser({
|
|
||||||
dataSource,
|
|
||||||
dateRange,
|
|
||||||
endDate = new Date(),
|
|
||||||
filters,
|
|
||||||
impersonationId,
|
|
||||||
startDate,
|
|
||||||
symbol,
|
|
||||||
user,
|
|
||||||
withExcludedAccounts
|
|
||||||
}: {
|
|
||||||
dateRange: DateRange;
|
|
||||||
endDate?: Date;
|
|
||||||
filters?: Filter[];
|
|
||||||
impersonationId: string;
|
|
||||||
startDate: Date;
|
|
||||||
user: UserWithSettings;
|
|
||||||
withExcludedAccounts?: boolean;
|
|
||||||
} & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
|
|
||||||
const marketData: { date: string; value: number }[] = [];
|
|
||||||
const userCurrency = user.Settings.settings.baseCurrency;
|
|
||||||
const userId = user.id;
|
|
||||||
|
|
||||||
const { chart } = await this.portfolioService.getPerformance({
|
|
||||||
dateRange,
|
|
||||||
filters,
|
|
||||||
impersonationId,
|
|
||||||
userId,
|
|
||||||
withExcludedAccounts
|
|
||||||
});
|
|
||||||
|
|
||||||
const [currentSymbolItem, marketDataItems] = await Promise.all([
|
|
||||||
this.symbolService.get({
|
|
||||||
dataGatheringItem: {
|
|
||||||
dataSource,
|
|
||||||
symbol
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
this.marketDataService.marketDataItems({
|
|
||||||
orderBy: {
|
|
||||||
date: 'asc'
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
dataSource,
|
|
||||||
symbol,
|
|
||||||
date: {
|
|
||||||
in: chart.map(({ date }) => {
|
|
||||||
return resetHours(parseDate(date));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
const exchangeRates =
|
|
||||||
await this.exchangeRateDataService.getExchangeRatesByCurrency({
|
|
||||||
startDate,
|
|
||||||
currencies: [currentSymbolItem.currency],
|
|
||||||
targetCurrency: userCurrency
|
|
||||||
});
|
|
||||||
|
|
||||||
const exchangeRateAtStartDate =
|
|
||||||
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
|
|
||||||
format(startDate, DATE_FORMAT)
|
|
||||||
];
|
|
||||||
|
|
||||||
const marketPriceAtStartDate = marketDataItems?.find(({ date }) => {
|
|
||||||
return isSameDay(date, startDate);
|
|
||||||
})?.marketPrice;
|
|
||||||
|
|
||||||
if (!marketPriceAtStartDate) {
|
|
||||||
Logger.error(
|
|
||||||
`No historical market data has been found for ${symbol} (${dataSource}) at ${format(
|
|
||||||
startDate,
|
|
||||||
DATE_FORMAT
|
|
||||||
)}`,
|
|
||||||
'BenchmarkService'
|
|
||||||
);
|
|
||||||
|
|
||||||
return { marketData };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const marketDataItem of marketDataItems) {
|
|
||||||
const exchangeRate =
|
|
||||||
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
|
|
||||||
format(marketDataItem.date, DATE_FORMAT)
|
|
||||||
];
|
|
||||||
|
|
||||||
const exchangeRateFactor =
|
|
||||||
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
|
|
||||||
? exchangeRate / exchangeRateAtStartDate
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
marketData.push({
|
|
||||||
date: format(marketDataItem.date, DATE_FORMAT),
|
|
||||||
value:
|
|
||||||
marketPriceAtStartDate === 0
|
|
||||||
? 0
|
|
||||||
: this.calculateChangeInPercentage(
|
|
||||||
marketPriceAtStartDate,
|
|
||||||
marketDataItem.marketPrice * exchangeRateFactor
|
|
||||||
) * 100
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const includesEndDate = isSameDay(
|
|
||||||
parseDate(marketData.at(-1).date),
|
|
||||||
endDate
|
|
||||||
);
|
|
||||||
|
|
||||||
if (currentSymbolItem?.marketPrice && !includesEndDate) {
|
|
||||||
const exchangeRate =
|
|
||||||
exchangeRates[`${currentSymbolItem.currency}${userCurrency}`]?.[
|
|
||||||
format(endDate, DATE_FORMAT)
|
|
||||||
];
|
|
||||||
|
|
||||||
const exchangeRateFactor =
|
|
||||||
isNumber(exchangeRateAtStartDate) && isNumber(exchangeRate)
|
|
||||||
? exchangeRate / exchangeRateAtStartDate
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
marketData.push({
|
|
||||||
date: format(endDate, DATE_FORMAT),
|
|
||||||
value:
|
|
||||||
this.calculateChangeInPercentage(
|
|
||||||
marketPriceAtStartDate,
|
|
||||||
currentSymbolItem.marketPrice * exchangeRateFactor
|
|
||||||
) * 100
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
marketData
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async addBenchmark({
|
public async addBenchmark({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
@ -1,5 +1,5 @@
|
|||||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
|
||||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||||
|
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
|
||||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||||
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
|
||||||
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
|
||||||
|
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
|
||||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||||
import {
|
import {
|
||||||
ghostfolioFearAndGreedIndexDataSource,
|
ghostfolioFearAndGreedIndexDataSource,
|
||||||
|
@ -303,7 +303,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
|
public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
|
||||||
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
|
return this.http.delete<any>(`/api/v1/benchmarks/${dataSource}/${symbol}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteOwnUser(aData: DeleteOwnUserDto) {
|
public deleteOwnUser(aData: DeleteOwnUserDto) {
|
||||||
@ -358,7 +358,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<BenchmarkMarketDataDetails>(
|
return this.http.get<BenchmarkMarketDataDetails>(
|
||||||
`/api/v1/benchmark/${dataSource}/${symbol}/${format(
|
`/api/v1/benchmarks/${dataSource}/${symbol}/${format(
|
||||||
startDate,
|
startDate,
|
||||||
DATE_FORMAT
|
DATE_FORMAT
|
||||||
)}`,
|
)}`,
|
||||||
@ -367,7 +367,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchBenchmarks() {
|
public fetchBenchmarks() {
|
||||||
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
return this.http.get<BenchmarkResponse>('/api/v1/benchmarks');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchExport({
|
public fetchExport({
|
||||||
@ -704,7 +704,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public postBenchmark(benchmark: AssetProfileIdentifier) {
|
public postBenchmark(benchmark: AssetProfileIdentifier) {
|
||||||
return this.http.post('/api/v1/benchmark', benchmark);
|
return this.http.post('/api/v1/benchmarks', benchmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
public postMarketData({
|
public postMarketData({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user