Feature/extend markets overview by benchmarks (#953)
* Add benchmarks to markets overview * Update changelog
This commit is contained in:
parent
4711b0d1ed
commit
2c4c16ec99
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Added
|
||||
|
||||
- Added the _Ghostfolio_ trailer to the landing page
|
||||
- Extended the markets overview by benchmarks (current change to the all time high)
|
||||
|
||||
## 1.151.0 - 24.05.2022
|
||||
|
||||
|
@ -20,6 +20,7 @@ import { AccountModule } from './account/account.module';
|
||||
import { AdminModule } from './admin/admin.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { BenchmarkModule } from './benchmark/benchmark.module';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
import { ExportModule } from './export/export.module';
|
||||
import { ImportModule } from './import/import.module';
|
||||
@ -37,6 +38,7 @@ import { UserModule } from './user/user.module';
|
||||
AccountModule,
|
||||
AuthDeviceModule,
|
||||
AuthModule,
|
||||
BenchmarkModule,
|
||||
BullModule.forRoot({
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST,
|
||||
|
32
apps/api/src/app/benchmark/benchmark.controller.ts
Normal file
32
apps/api/src/app/benchmark/benchmark.controller.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
import { BenchmarkService } from './benchmark.service';
|
||||
|
||||
@Controller('benchmark')
|
||||
export class BenchmarkController {
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly propertyService: PropertyService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||
const benchmarkAssets: UniqueAsset[] =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as UniqueAsset[]) ?? [];
|
||||
|
||||
return {
|
||||
benchmarks: await this.benchmarkService.getBenchmarks(benchmarkAssets)
|
||||
};
|
||||
}
|
||||
}
|
24
apps/api/src/app/benchmark/benchmark.module.ts
Normal file
24
apps/api/src/app/benchmark/benchmark.module.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { BenchmarkController } from './benchmark.controller';
|
||||
import { BenchmarkService } from './benchmark.service';
|
||||
|
||||
@Module({
|
||||
controllers: [BenchmarkController],
|
||||
imports: [
|
||||
ConfigurationModule,
|
||||
DataProviderModule,
|
||||
MarketDataModule,
|
||||
PropertyModule,
|
||||
RedisCacheModule,
|
||||
SymbolProfileModule
|
||||
],
|
||||
providers: [BenchmarkService]
|
||||
})
|
||||
export class BenchmarkModule {}
|
77
apps/api/src/app/benchmark/benchmark.service.ts
Normal file
77
apps/api/src/app/benchmark/benchmark.service.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Big from 'big.js';
|
||||
|
||||
@Injectable()
|
||||
export class BenchmarkService {
|
||||
private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS';
|
||||
|
||||
public constructor(
|
||||
private readonly dataProviderService: DataProviderService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly redisCacheService: RedisCacheService,
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public async getBenchmarks(
|
||||
benchmarkAssets: UniqueAsset[]
|
||||
): Promise<BenchmarkResponse['benchmarks']> {
|
||||
let benchmarks: BenchmarkResponse['benchmarks'];
|
||||
|
||||
try {
|
||||
benchmarks = JSON.parse(
|
||||
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
|
||||
);
|
||||
|
||||
if (benchmarks) {
|
||||
return benchmarks;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const promises: Promise<number>[] = [];
|
||||
|
||||
const [quotes, assetProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes(benchmarkAssets),
|
||||
this.symbolProfileService.getSymbolProfiles(benchmarkAssets)
|
||||
]);
|
||||
|
||||
for (const benchmarkAsset of benchmarkAssets) {
|
||||
promises.push(this.marketDataService.getMax(benchmarkAsset));
|
||||
}
|
||||
|
||||
const allTimeHighs = await Promise.all(promises);
|
||||
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
||||
|
||||
const performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||
.div(allTimeHigh)
|
||||
.minus(1);
|
||||
|
||||
return {
|
||||
name: assetProfiles.find(({ dataSource, symbol }) => {
|
||||
return (
|
||||
dataSource === benchmarkAssets[index].dataSource &&
|
||||
symbol === benchmarkAssets[index].symbol
|
||||
);
|
||||
})?.name,
|
||||
performances: {
|
||||
allTimeHigh: {
|
||||
performancePercent: performancePercentFromAllTimeHigh.toNumber()
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
await this.redisCacheService.set(
|
||||
this.CACHE_KEY_BENCHMARKS,
|
||||
JSON.stringify(benchmarks)
|
||||
);
|
||||
|
||||
return benchmarks;
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
EnhancedSymbolProfile,
|
||||
HistoricalDataItem
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { Tag } from '@prisma/client';
|
||||
|
||||
|
@ -19,7 +19,6 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import {
|
||||
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||
@ -28,6 +27,7 @@ import {
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
EnhancedSymbolProfile,
|
||||
Filter,
|
||||
HistoricalDataItem,
|
||||
PortfolioDetails,
|
||||
@ -375,7 +375,7 @@ export class PortfolioService {
|
||||
|
||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes(dataGatheringItems),
|
||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||
this.symbolProfileService.getSymbolProfilesBySymbols(symbols)
|
||||
]);
|
||||
|
||||
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
||||
@ -518,9 +518,8 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
const positionCurrency = orders[0].SymbolProfile.currency;
|
||||
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
||||
aSymbol
|
||||
]);
|
||||
const [SymbolProfile] =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols([aSymbol]);
|
||||
|
||||
const portfolioOrders: PortfolioOrder[] = orders
|
||||
.filter((order) => {
|
||||
@ -768,7 +767,7 @@ export class PortfolioService {
|
||||
|
||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||
this.dataProviderService.getQuotes(dataGatheringItem),
|
||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
||||
this.symbolProfileService.getSymbolProfilesBySymbols(symbols)
|
||||
]);
|
||||
|
||||
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { HistoricalDataItem, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export interface SymbolItem {
|
||||
export interface SymbolItem extends UniqueAsset {
|
||||
currency: string;
|
||||
dataSource: DataSource;
|
||||
historicalData: HistoricalDataItem[];
|
||||
marketPrice: number;
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ export class SymbolService {
|
||||
currency,
|
||||
historicalData,
|
||||
marketPrice,
|
||||
dataSource: dataGatheringItem.dataSource
|
||||
dataSource: dataGatheringItem.dataSource,
|
||||
symbol: dataGatheringItem.symbol
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -247,11 +247,12 @@ export class DataGatheringService {
|
||||
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
||||
uniqueAssets
|
||||
);
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
uniqueAssets.map(({ symbol }) => {
|
||||
return symbol;
|
||||
})
|
||||
);
|
||||
const symbolProfiles =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols(
|
||||
uniqueAssets.map(({ symbol }) => {
|
||||
return symbol;
|
||||
})
|
||||
);
|
||||
|
||||
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
||||
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
||||
|
@ -46,9 +46,8 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
try {
|
||||
const symbol = aSymbol;
|
||||
|
||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
||||
[symbol]
|
||||
);
|
||||
const [symbolProfile] =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols([symbol]);
|
||||
const { defaultMarketPrice, selector, url } =
|
||||
symbolProfile.scraperConfiguration;
|
||||
|
||||
@ -108,9 +107,8 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
}
|
||||
|
||||
try {
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
aSymbols
|
||||
);
|
||||
const symbolProfiles =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols(aSymbols);
|
||||
|
||||
const marketData = await this.prismaService.marketData.findMany({
|
||||
distinct: ['symbol'],
|
||||
|
@ -91,9 +91,8 @@ export class GoogleSheetsService implements DataProviderInterface {
|
||||
try {
|
||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||
|
||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
aSymbols
|
||||
);
|
||||
const symbolProfiles =
|
||||
await this.symbolProfileService.getSymbolProfilesBySymbols(aSymbols);
|
||||
|
||||
const sheet = await this.getSheet({
|
||||
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'),
|
||||
|
@ -34,6 +34,20 @@ export class MarketDataService {
|
||||
});
|
||||
}
|
||||
|
||||
public async getMax({ dataSource, symbol }: UniqueAsset): Promise<number> {
|
||||
const aggregations = await this.prismaService.marketData.aggregate({
|
||||
_max: {
|
||||
marketPrice: true
|
||||
},
|
||||
where: {
|
||||
dataSource,
|
||||
symbol
|
||||
}
|
||||
});
|
||||
|
||||
return aggregations._max.marketPrice;
|
||||
}
|
||||
|
||||
public async getRange({
|
||||
dateQuery,
|
||||
symbols
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import {
|
||||
EnhancedSymbolProfile,
|
||||
ScraperConfiguration,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
@ -12,8 +16,6 @@ import {
|
||||
} from '@prisma/client';
|
||||
import { continents, countries } from 'countries-list';
|
||||
|
||||
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
||||
|
||||
@Injectable()
|
||||
export class SymbolProfileService {
|
||||
public constructor(private readonly prismaService: PrismaService) {}
|
||||
@ -37,6 +39,35 @@ export class SymbolProfileService {
|
||||
}
|
||||
|
||||
public async getSymbolProfiles(
|
||||
aUniqueAssets: UniqueAsset[]
|
||||
): Promise<EnhancedSymbolProfile[]> {
|
||||
return this.prismaService.symbolProfile
|
||||
.findMany({
|
||||
include: { SymbolProfileOverrides: true },
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
dataSource: {
|
||||
in: aUniqueAssets.map(({ dataSource }) => {
|
||||
return dataSource;
|
||||
})
|
||||
},
|
||||
symbol: {
|
||||
in: aUniqueAssets.map(({ symbol }) => {
|
||||
return symbol;
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public async getSymbolProfilesBySymbols(
|
||||
symbols: string[]
|
||||
): Promise<EnhancedSymbolProfile[]> {
|
||||
return this.prismaService.symbolProfile
|
||||
|
@ -4,6 +4,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Benchmark,
|
||||
HistoricalDataItem,
|
||||
InfoItem,
|
||||
User
|
||||
@ -18,6 +19,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './home-market.html'
|
||||
})
|
||||
export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
public benchmarks: Benchmark[];
|
||||
public fearAndGreedIndex: number;
|
||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||
public historicalData: HistoricalDataItem[];
|
||||
@ -73,6 +75,15 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchBenchmarks()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ benchmarks }) => {
|
||||
this.benchmarks = benchmarks;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
@ -1,13 +1,12 @@
|
||||
<div
|
||||
class="align-items-center container d-flex flex-grow-1 h-100 justify-content-center w-100"
|
||||
>
|
||||
<div class="no-gutters row w-100">
|
||||
<div class="container">
|
||||
<h3 class="mb-3 text-center" i18n>Markets</h3>
|
||||
<div class="mb-5 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<div class="mb-2 text-center text-muted">
|
||||
<small i18n>Last {{ numberOfDays }} Days</small>
|
||||
</div>
|
||||
<gf-line-chart
|
||||
class="mb-5"
|
||||
class="mb-3"
|
||||
yMax="100"
|
||||
yMaxLabel="Greed"
|
||||
yMin="0"
|
||||
@ -23,4 +22,20 @@
|
||||
></gf-fear-and-greed-index>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row">
|
||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||
<gf-benchmark
|
||||
*ngFor="let benchmark of benchmarks"
|
||||
class="py-2"
|
||||
[benchmark]="benchmark"
|
||||
[locale]="user?.settings?.locale"
|
||||
></gf-benchmark>
|
||||
<gf-benchmark
|
||||
*ngIf="!benchmarks"
|
||||
class="py-2"
|
||||
[benchmark]="undefined"
|
||||
></gf-benchmark>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
||||
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
|
||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||
|
||||
import { HomeMarketComponent } from './home-market.component';
|
||||
@ -8,7 +9,12 @@ import { HomeMarketComponent } from './home-market.component';
|
||||
@NgModule({
|
||||
declarations: [HomeMarketComponent],
|
||||
exports: [],
|
||||
imports: [CommonModule, GfFearAndGreedIndexModule, GfLineChartModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfBenchmarkModule,
|
||||
GfFearAndGreedIndexModule,
|
||||
GfLineChartModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
|
@ -7,9 +7,9 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||
import { Tag } from '@prisma/client';
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
Accounts,
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
BenchmarkResponse,
|
||||
Export,
|
||||
Filter,
|
||||
InfoItem,
|
||||
@ -90,6 +91,10 @@ export class DataService {
|
||||
return this.http.get<Access[]>('/api/v1/access');
|
||||
}
|
||||
|
||||
public fetchBenchmarks() {
|
||||
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
||||
}
|
||||
|
||||
public fetchChart({ range }: { range: DateRange }) {
|
||||
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
||||
params: { range }
|
||||
|
@ -60,12 +60,8 @@ body {
|
||||
}
|
||||
|
||||
ngx-skeleton-loader {
|
||||
line-height: 0;
|
||||
outline: 0;
|
||||
|
||||
.loader {
|
||||
background-color: #323232;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,9 +113,13 @@ ion-icon {
|
||||
|
||||
ngx-skeleton-loader {
|
||||
display: block;
|
||||
line-height: 0;
|
||||
outline: 0;
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
margin: 0 !important;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
||||
|
||||
export const GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
|
||||
|
||||
export const PROPERTY_BENCHMARKS = 'BENCHMARKS';
|
||||
export const PROPERTY_COUPONS = 'COUPONS';
|
||||
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
||||
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
|
||||
|
10
libs/common/src/lib/interfaces/benchmark.interface.ts
Normal file
10
libs/common/src/lib/interfaces/benchmark.interface.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||
|
||||
export interface Benchmark {
|
||||
name: EnhancedSymbolProfile['name'];
|
||||
performances: {
|
||||
allTimeHigh: {
|
||||
performancePercent: number;
|
||||
};
|
||||
};
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { ScraperConfiguration } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||
|
||||
import { Country } from './country.interface';
|
||||
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||
import { Sector } from './sector.interface';
|
||||
|
||||
export interface EnhancedSymbolProfile {
|
||||
assetClass: AssetClass;
|
||||
assetSubClass: AssetSubClass;
|
@ -6,7 +6,9 @@ import {
|
||||
AdminMarketData,
|
||||
AdminMarketDataItem
|
||||
} from './admin-market-data.interface';
|
||||
import { Benchmark } from './benchmark.interface';
|
||||
import { Coupon } from './coupon.interface';
|
||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||
import { Export } from './export.interface';
|
||||
import { FilterGroup } from './filter-group.interface';
|
||||
import { Filter } from './filter.interface';
|
||||
@ -24,8 +26,10 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
||||
import { PortfolioReport } from './portfolio-report.interface';
|
||||
import { PortfolioSummary } from './portfolio-summary.interface';
|
||||
import { Position } from './position.interface';
|
||||
import { BenchmarkResponse } from './responses/benchmark-response.interface';
|
||||
import { ResponseError } from './responses/errors.interface';
|
||||
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||
import { TimelinePosition } from './timeline-position.interface';
|
||||
import { UniqueAsset } from './unique-asset.interface';
|
||||
import { UserSettings } from './user-settings.interface';
|
||||
@ -39,7 +43,10 @@ export {
|
||||
AdminMarketData,
|
||||
AdminMarketDataDetails,
|
||||
AdminMarketDataItem,
|
||||
Benchmark,
|
||||
BenchmarkResponse,
|
||||
Coupon,
|
||||
EnhancedSymbolProfile,
|
||||
Export,
|
||||
Filter,
|
||||
FilterGroup,
|
||||
@ -59,6 +66,7 @@ export {
|
||||
PortfolioSummary,
|
||||
Position,
|
||||
ResponseError,
|
||||
ScraperConfiguration,
|
||||
TimelinePosition,
|
||||
UniqueAsset,
|
||||
User,
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { Benchmark } from '../benchmark.interface';
|
||||
|
||||
export interface BenchmarkResponse {
|
||||
benchmarks: Benchmark[];
|
||||
}
|
32
libs/ui/src/lib/benchmark/benchmark.component.html
Normal file
32
libs/ui/src/lib/benchmark/benchmark.component.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div class="align-items-center d-flex">
|
||||
<div *ngIf="benchmark?.name" class="flex-grow-1 text-truncate">
|
||||
{{ benchmark.name }}
|
||||
</div>
|
||||
<div *ngIf="!benchmark?.name" class="flex-grow-1">
|
||||
<ngx-skeleton-loader
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
width: '15rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
</div>
|
||||
<gf-value
|
||||
class="mx-2"
|
||||
size="medium"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[ngClass]="{
|
||||
'text-danger':
|
||||
benchmark?.performances?.allTimeHigh?.performancePercent < 0,
|
||||
'text-success':
|
||||
benchmark?.performances?.allTimeHigh?.performancePercent > 0
|
||||
}"
|
||||
[value]="
|
||||
benchmark?.performances?.allTimeHigh?.performancePercent ?? undefined
|
||||
"
|
||||
></gf-value>
|
||||
<div class="text-muted">
|
||||
<small class="d-none d-sm-block text-nowrap" i18n>from All Time High</small
|
||||
><small class="d-block d-sm-none text-nowrap" i18n>from ATH</small>
|
||||
</div>
|
||||
</div>
|
3
libs/ui/src/lib/benchmark/benchmark.component.scss
Normal file
3
libs/ui/src/lib/benchmark/benchmark.component.scss
Normal file
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
15
libs/ui/src/lib/benchmark/benchmark.component.ts
Normal file
15
libs/ui/src/lib/benchmark/benchmark.component.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { Benchmark } from '@ghostfolio/common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-benchmark',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './benchmark.component.html',
|
||||
styleUrls: ['./benchmark.component.scss']
|
||||
})
|
||||
export class BenchmarkComponent {
|
||||
@Input() benchmark: Benchmark;
|
||||
@Input() locale: string;
|
||||
|
||||
public constructor() {}
|
||||
}
|
14
libs/ui/src/lib/benchmark/benchmark.module.ts
Normal file
14
libs/ui/src/lib/benchmark/benchmark.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { GfValueModule } from '../value';
|
||||
import { BenchmarkComponent } from './benchmark.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [BenchmarkComponent],
|
||||
exports: [BenchmarkComponent],
|
||||
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfBenchmarkModule {}
|
1
libs/ui/src/lib/benchmark/index.ts
Normal file
1
libs/ui/src/lib/benchmark/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './benchmark.module';
|
@ -58,7 +58,7 @@
|
||||
*ngIf="value === undefined"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: size === 'large' ? '2.5rem' : '1.5rem',
|
||||
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
|
||||
width: '5rem'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
|
Loading…
x
Reference in New Issue
Block a user