Feature/set asset profile as benchmark (#2002)
* Set asset profile as benchmark * Update changelog Co-authored-by: Arghya Ghosh <arghyag5@gmail.com>
This commit is contained in:
parent
1916e5343d
commit
215f5eafa6
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support to set an asset profile as a benchmark
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Decreased the density of the `@angular/material` tables
|
- 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import {
|
import {
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
BenchmarkResponse
|
BenchmarkResponse,
|
||||||
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
HttpException,
|
||||||
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
|
Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import { BenchmarkService } from './benchmark.service';
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
@Controller('benchmark')
|
@Controller('benchmark')
|
||||||
export class BenchmarkController {
|
export class BenchmarkController {
|
||||||
public constructor(private readonly benchmarkService: BenchmarkService) {}
|
public constructor(
|
||||||
|
private readonly benchmarkService: BenchmarkService,
|
||||||
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
@ -45,4 +57,41 @@ export class BenchmarkController {
|
|||||||
symbol
|
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 { 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 { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.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 { PropertyModule } from '@ghostfolio/api/services/property/property.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';
|
||||||
@ -17,6 +18,7 @@ import { BenchmarkService } from './benchmark.service';
|
|||||||
ConfigurationModule,
|
ConfigurationModule,
|
||||||
DataProviderModule,
|
DataProviderModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
|
PrismaModule,
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
RedisCacheModule,
|
RedisCacheModule,
|
||||||
SymbolModule,
|
SymbolModule,
|
||||||
|
@ -4,7 +4,15 @@ describe('BenchmarkService', () => {
|
|||||||
let benchmarkService: BenchmarkService;
|
let benchmarkService: BenchmarkService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
benchmarkService = new BenchmarkService(null, null, null, null, null, null);
|
benchmarkService = new BenchmarkService(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculateChangeInPercentage', async () => {
|
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 { 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 { 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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +12,7 @@ import {
|
|||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
|
BenchmarkProperty,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
@ -18,6 +20,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { SymbolProfile } from '@prisma/client';
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -27,6 +30,7 @@ export class BenchmarkService {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService,
|
private readonly propertyService: PropertyService,
|
||||||
private readonly redisCacheService: RedisCacheService,
|
private readonly redisCacheService: RedisCacheService,
|
||||||
private readonly symbolProfileService: SymbolProfileService,
|
private readonly symbolProfileService: SymbolProfileService,
|
||||||
@ -116,9 +120,9 @@ export class BenchmarkService {
|
|||||||
|
|
||||||
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||||
const symbolProfileIds: string[] = (
|
const symbolProfileIds: string[] = (
|
||||||
((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as {
|
((await this.propertyService.getByKey(
|
||||||
symbolProfileId: string;
|
PROPERTY_BENCHMARKS
|
||||||
}[]) ?? []
|
)) as BenchmarkProperty[]) ?? []
|
||||||
).map(({ symbolProfileId }) => {
|
).map(({ symbolProfileId }) => {
|
||||||
return symbolProfileId;
|
return symbolProfileId;
|
||||||
});
|
});
|
||||||
@ -204,6 +208,43 @@ export class BenchmarkService {
|
|||||||
return response;
|
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) {
|
private getMarketCondition(aPerformanceInPercent: number) {
|
||||||
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||||
}
|
}
|
||||||
|
@ -143,18 +143,6 @@
|
|||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
|
<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
|
<button
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
[disabled]="element.activitiesCount !== 0"
|
[disabled]="element.activitiesCount !== 0"
|
||||||
|
@ -10,13 +10,13 @@ import { FormBuilder } from '@angular/forms';
|
|||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import {
|
import {
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
EnhancedSymbolProfile,
|
|
||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
import { MarketData } from '@prisma/client';
|
import { MarketData, SymbolProfile } from '@prisma/client';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -37,9 +37,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
symbolMapping: ''
|
symbolMapping: ''
|
||||||
});
|
});
|
||||||
public assetSubClass: string;
|
public assetSubClass: string;
|
||||||
|
public benchmarks: Partial<SymbolProfile>[];
|
||||||
public countries: {
|
public countries: {
|
||||||
[code: string]: { name: string; value: number };
|
[code: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
|
public isBenchmark = false;
|
||||||
public marketDataDetails: MarketData[] = [];
|
public marketDataDetails: MarketData[] = [];
|
||||||
public sectors: {
|
public sectors: {
|
||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
@ -51,11 +53,14 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
private adminService: AdminService,
|
private adminService: AdminService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
||||||
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
||||||
private formBuilder: FormBuilder
|
private formBuilder: FormBuilder
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public ngOnInit(): void {
|
public ngOnInit(): void {
|
||||||
|
this.benchmarks = this.dataService.fetchInfo().benchmarks;
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +77,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
|||||||
this.assetClass = translate(this.assetProfile?.assetClass);
|
this.assetClass = translate(this.assetProfile?.assetClass);
|
||||||
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
|
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
|
||||||
this.countries = {};
|
this.countries = {};
|
||||||
|
this.isBenchmark = this.benchmarks.some(({ id }) => {
|
||||||
|
return id === this.assetProfile.id;
|
||||||
|
});
|
||||||
this.marketDataDetails = marketData;
|
this.marketDataDetails = marketData;
|
||||||
this.sectors = {};
|
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() {
|
public onSubmit() {
|
||||||
let symbolMapping = {};
|
let symbolMapping = {};
|
||||||
|
|
||||||
|
@ -37,6 +37,13 @@
|
|||||||
>
|
>
|
||||||
<ng-container i18n>Gather Profile Data</ng-container>
|
<ng-container i18n>Gather Profile Data</ng-container>
|
||||||
</button>
|
</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>
|
</mat-menu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -405,6 +405,10 @@ export class DataService {
|
|||||||
return this.http.post<OrderModel>(`/api/v1/account`, aAccount);
|
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) {
|
public postOrder(aOrder: CreateOrderDto) {
|
||||||
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
|
return this.http.post<OrderModel>(`/api/v1/order`, aOrder);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export interface BenchmarkProperty {
|
||||||
|
symbolProfileId: string;
|
||||||
|
}
|
@ -8,6 +8,7 @@ import {
|
|||||||
AdminMarketDataItem
|
AdminMarketDataItem
|
||||||
} from './admin-market-data.interface';
|
} from './admin-market-data.interface';
|
||||||
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
|
import { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
|
||||||
|
import { BenchmarkProperty } from './benchmark-property.interface';
|
||||||
import { Benchmark } from './benchmark.interface';
|
import { Benchmark } from './benchmark.interface';
|
||||||
import { Coupon } from './coupon.interface';
|
import { Coupon } from './coupon.interface';
|
||||||
import { DataProviderInfo } from './data-provider-info.interface';
|
import { DataProviderInfo } from './data-provider-info.interface';
|
||||||
@ -54,6 +55,7 @@ export {
|
|||||||
AdminMarketDataItem,
|
AdminMarketDataItem,
|
||||||
Benchmark,
|
Benchmark,
|
||||||
BenchmarkMarketDataDetails,
|
BenchmarkMarketDataDetails,
|
||||||
|
BenchmarkProperty,
|
||||||
BenchmarkResponse,
|
BenchmarkResponse,
|
||||||
Coupon,
|
Coupon,
|
||||||
DataProviderInfo,
|
DataProviderInfo,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user