From 215f5eafa64aa2e6f2c15acbeeaa05dc55ccb715 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 24 May 2023 21:22:32 +0200 Subject: [PATCH] Feature/set asset profile as benchmark (#2002) * Set asset profile as benchmark * Update changelog Co-authored-by: Arghya Ghosh --- CHANGELOG.md | 4 ++ .../src/app/benchmark/benchmark.controller.ts | 53 ++++++++++++++++++- .../api/src/app/benchmark/benchmark.module.ts | 2 + .../app/benchmark/benchmark.service.spec.ts | 10 +++- .../src/app/benchmark/benchmark.service.ts | 47 ++++++++++++++-- .../admin-market-data/admin-market-data.html | 12 ----- .../asset-profile-dialog.component.ts | 23 +++++++- .../asset-profile-dialog.html | 7 +++ apps/client/src/app/services/data.service.ts | 4 ++ .../benchmark-property.interface.ts | 3 ++ libs/common/src/lib/interfaces/index.ts | 2 + 11 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 libs/common/src/lib/interfaces/benchmark-property.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ef017ba04..662e1f3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support to set an asset profile as a benchmark + ### Changed - Decreased the density of the `@angular/material` tables diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index 4daa14009..91426090e 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -2,23 +2,35 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { BenchmarkMarketDataDetails, - BenchmarkResponse + BenchmarkResponse, + UniqueAsset } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import type { RequestWithUser } from '@ghostfolio/common/types'; import { + Body, Controller, Get, + HttpException, + Inject, Param, + Post, UseGuards, UseInterceptors } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; import { DataSource } from '@prisma/client'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { BenchmarkService } from './benchmark.service'; @Controller('benchmark') export class BenchmarkController { - public constructor(private readonly benchmarkService: BenchmarkService) {} + public constructor( + private readonly benchmarkService: BenchmarkService, + @Inject(REQUEST) private readonly request: RequestWithUser + ) {} @Get() @UseInterceptors(TransformDataSourceInRequestInterceptor) @@ -45,4 +57,41 @@ export class BenchmarkController { symbol }); } + + @Post() + @UseGuards(AuthGuard('jwt')) + public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + try { + const benchmark = await this.benchmarkService.addBenchmark({ + dataSource, + symbol + }); + + if (!benchmark) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return benchmark; + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + StatusCodes.INTERNAL_SERVER_ERROR + ); + } + } } diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/benchmark/benchmark.module.ts index 71b08c191..c2cc3fbb5 100644 --- a/apps/api/src/app/benchmark/benchmark.module.ts +++ b/apps/api/src/app/benchmark/benchmark.module.ts @@ -3,6 +3,7 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.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'; @@ -17,6 +18,7 @@ import { BenchmarkService } from './benchmark.service'; ConfigurationModule, DataProviderModule, MarketDataModule, + PrismaModule, PropertyModule, RedisCacheModule, SymbolModule, diff --git a/apps/api/src/app/benchmark/benchmark.service.spec.ts b/apps/api/src/app/benchmark/benchmark.service.spec.ts index 833dbcdfc..5fa2c3e7b 100644 --- a/apps/api/src/app/benchmark/benchmark.service.spec.ts +++ b/apps/api/src/app/benchmark/benchmark.service.spec.ts @@ -4,7 +4,15 @@ describe('BenchmarkService', () => { let benchmarkService: BenchmarkService; beforeAll(async () => { - benchmarkService = new BenchmarkService(null, null, null, null, null, null); + benchmarkService = new BenchmarkService( + null, + null, + null, + null, + null, + null, + null + ); }); it('calculateChangeInPercentage', async () => { diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 4e87b26f9..73b48068b 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -2,6 +2,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { @@ -11,6 +12,7 @@ import { import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { BenchmarkMarketDataDetails, + BenchmarkProperty, BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; @@ -18,6 +20,7 @@ import { Injectable } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import Big from 'big.js'; import { format } from 'date-fns'; +import { uniqBy } from 'lodash'; import ms from 'ms'; @Injectable() @@ -27,6 +30,7 @@ export class BenchmarkService { public constructor( private readonly dataProviderService: DataProviderService, private readonly marketDataService: MarketDataService, + private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, private readonly symbolProfileService: SymbolProfileService, @@ -116,9 +120,9 @@ export class BenchmarkService { public async getBenchmarkAssetProfiles(): Promise[]> { const symbolProfileIds: string[] = ( - ((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as { - symbolProfileId: string; - }[]) ?? [] + ((await this.propertyService.getByKey( + PROPERTY_BENCHMARKS + )) as BenchmarkProperty[]) ?? [] ).map(({ symbolProfileId }) => { return symbolProfileId; }); @@ -204,6 +208,43 @@ export class BenchmarkService { return response; } + public async addBenchmark({ + dataSource, + symbol + }: UniqueAsset): Promise> { + const assetProfile = await this.prismaService.symbolProfile.findFirst({ + where: { + dataSource, + symbol + } + }); + + if (!assetProfile) { + return; + } + + let benchmarks = + ((await this.propertyService.getByKey( + PROPERTY_BENCHMARKS + )) as BenchmarkProperty[]) ?? []; + + benchmarks.push({ symbolProfileId: assetProfile.id }); + + benchmarks = uniqBy(benchmarks, 'symbolProfileId'); + + await this.propertyService.put({ + key: PROPERTY_BENCHMARKS, + value: JSON.stringify(benchmarks) + }); + + return { + dataSource, + symbol, + id: assetProfile.id, + name: assetProfile.name + }; + } + private getMarketCondition(aPerformanceInPercent: number) { return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; } diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index 36beae6dd..bb9322a68 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -143,18 +143,6 @@ - - + diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index bff672717..32e7ba7ae 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -405,6 +405,10 @@ export class DataService { return this.http.post(`/api/v1/account`, aAccount); } + public postBenchmark(benchmark: UniqueAsset) { + return this.http.post(`/api/v1/benchmark`, benchmark); + } + public postOrder(aOrder: CreateOrderDto) { return this.http.post(`/api/v1/order`, aOrder); } diff --git a/libs/common/src/lib/interfaces/benchmark-property.interface.ts b/libs/common/src/lib/interfaces/benchmark-property.interface.ts new file mode 100644 index 000000000..bccf4ed78 --- /dev/null +++ b/libs/common/src/lib/interfaces/benchmark-property.interface.ts @@ -0,0 +1,3 @@ +export interface BenchmarkProperty { + symbolProfileId: string; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index b6b1e79c7..538615861 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -8,6 +8,7 @@ import { AdminMarketDataItem } from './admin-market-data.interface'; import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; +import { BenchmarkProperty } from './benchmark-property.interface'; import { Benchmark } from './benchmark.interface'; import { Coupon } from './coupon.interface'; import { DataProviderInfo } from './data-provider-info.interface'; @@ -54,6 +55,7 @@ export { AdminMarketDataItem, Benchmark, BenchmarkMarketDataDetails, + BenchmarkProperty, BenchmarkResponse, Coupon, DataProviderInfo,