Feature/set asset profile as benchmark (#2002)

* Set asset profile as benchmark

* Update changelog

Co-authored-by: Arghya Ghosh <arghyag5@gmail.com>
pull/2005/head
Thomas Kaul 1 year ago committed by GitHub
parent 1916e5343d
commit 215f5eafa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -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
);
}
}
}

@ -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,

@ -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 () => {

@ -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<Partial<SymbolProfile>[]> {
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<Partial<SymbolProfile>> {
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';
}

@ -143,18 +143,6 @@
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
>
<ng-container i18n>Gather Historical Data</ng-container>
</button>
<button
mat-menu-item
(click)="onGatherProfileDataBySymbol({dataSource: element.dataSource, symbol: element.symbol})"
>
<ng-container i18n>Gather Profile Data</ng-container>
</button>
<button
mat-menu-item
[disabled]="element.activitiesCount !== 0"

@ -10,13 +10,13 @@ import { FormBuilder } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import {
AdminMarketDataDetails,
EnhancedSymbolProfile,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import { MarketData } from '@prisma/client';
import { MarketData, SymbolProfile } from '@prisma/client';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -37,9 +37,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
symbolMapping: ''
});
public assetSubClass: string;
public benchmarks: Partial<SymbolProfile>[];
public countries: {
[code: string]: { name: string; value: number };
};
public isBenchmark = false;
public marketDataDetails: MarketData[] = [];
public sectors: {
[name: string]: { name: string; value: number };
@ -51,11 +53,14 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AssetProfileDialog>,
private formBuilder: FormBuilder
) {}
public ngOnInit(): void {
this.benchmarks = this.dataService.fetchInfo().benchmarks;
this.initialize();
}
@ -72,6 +77,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.assetClass = translate(this.assetProfile?.assetClass);
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
this.countries = {};
this.isBenchmark = this.benchmarks.some(({ id }) => {
return id === this.assetProfile.id;
});
this.marketDataDetails = marketData;
this.sectors = {};
@ -128,6 +136,17 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}
}
public onSetBenchmark({ dataSource, symbol }: UniqueAsset) {
this.dataService
.postBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
setTimeout(() => {
window.location.reload();
}, 300);
});
}
public onSubmit() {
let symbolMapping = {};

@ -37,6 +37,13 @@
>
<ng-container i18n>Gather Profile Data</ng-container>
</button>
<button
mat-menu-item
[disabled]="isBenchmark"
(click)="onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
>
<ng-container i18n>Set as Benchmark</ng-container>
</button>
</mat-menu>
</div>

@ -405,6 +405,10 @@ export class DataService {
return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
}
public postBenchmark(benchmark: UniqueAsset) {
return this.http.post(`/api/v1/benchmark`, benchmark);
}
public postOrder(aOrder: CreateOrderDto) {
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
}

@ -0,0 +1,3 @@
export interface BenchmarkProperty {
symbolProfileId: string;
}

@ -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,

Loading…
Cancel
Save