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
|
||||||
|
|
||||||
- Added the _Ghostfolio_ trailer to the landing page
|
- 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
|
## 1.151.0 - 24.05.2022
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import { AccountModule } from './account/account.module';
|
|||||||
import { AdminModule } from './admin/admin.module';
|
import { AdminModule } from './admin/admin.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
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 { ExportModule } from './export/export.module';
|
import { ExportModule } from './export/export.module';
|
||||||
import { ImportModule } from './import/import.module';
|
import { ImportModule } from './import/import.module';
|
||||||
@ -37,6 +38,7 @@ import { UserModule } from './user/user.module';
|
|||||||
AccountModule,
|
AccountModule,
|
||||||
AuthDeviceModule,
|
AuthDeviceModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
BenchmarkModule,
|
||||||
BullModule.forRoot({
|
BullModule.forRoot({
|
||||||
redis: {
|
redis: {
|
||||||
host: process.env.REDIS_HOST,
|
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 {
|
||||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
EnhancedSymbolProfile,
|
||||||
|
HistoricalDataItem
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { Tag } from '@prisma/client';
|
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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import {
|
import {
|
||||||
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
@ -28,6 +27,7 @@ import {
|
|||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
|
EnhancedSymbolProfile,
|
||||||
Filter,
|
Filter,
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
@ -375,7 +375,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes(dataGatheringItems),
|
this.dataProviderService.getQuotes(dataGatheringItems),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfilesBySymbols(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
||||||
@ -518,9 +518,8 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const positionCurrency = orders[0].SymbolProfile.currency;
|
const positionCurrency = orders[0].SymbolProfile.currency;
|
||||||
const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([
|
const [SymbolProfile] =
|
||||||
aSymbol
|
await this.symbolProfileService.getSymbolProfilesBySymbols([aSymbol]);
|
||||||
]);
|
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders
|
const portfolioOrders: PortfolioOrder[] = orders
|
||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
@ -768,7 +767,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
const [dataProviderResponses, symbolProfiles] = await Promise.all([
|
||||||
this.dataProviderService.getQuotes(dataGatheringItem),
|
this.dataProviderService.getQuotes(dataGatheringItem),
|
||||||
this.symbolProfileService.getSymbolProfiles(symbols)
|
this.symbolProfileService.getSymbolProfilesBySymbols(symbols)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {};
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
import { HistoricalDataItem, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface SymbolItem {
|
export interface SymbolItem extends UniqueAsset {
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
|
||||||
historicalData: HistoricalDataItem[];
|
historicalData: HistoricalDataItem[];
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,8 @@ export class SymbolService {
|
|||||||
currency,
|
currency,
|
||||||
historicalData,
|
historicalData,
|
||||||
marketPrice,
|
marketPrice,
|
||||||
dataSource: dataGatheringItem.dataSource
|
dataSource: dataGatheringItem.dataSource,
|
||||||
|
symbol: dataGatheringItem.symbol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,11 +247,12 @@ export class DataGatheringService {
|
|||||||
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
const assetProfiles = await this.dataProviderService.getAssetProfiles(
|
||||||
uniqueAssets
|
uniqueAssets
|
||||||
);
|
);
|
||||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
const symbolProfiles =
|
||||||
uniqueAssets.map(({ symbol }) => {
|
await this.symbolProfileService.getSymbolProfilesBySymbols(
|
||||||
return symbol;
|
uniqueAssets.map(({ symbol }) => {
|
||||||
})
|
return symbol;
|
||||||
);
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
|
||||||
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
const symbolMapping = symbolProfiles.find((symbolProfile) => {
|
||||||
|
@ -46,9 +46,8 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const symbol = aSymbol;
|
const symbol = aSymbol;
|
||||||
|
|
||||||
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
|
const [symbolProfile] =
|
||||||
[symbol]
|
await this.symbolProfileService.getSymbolProfilesBySymbols([symbol]);
|
||||||
);
|
|
||||||
const { defaultMarketPrice, selector, url } =
|
const { defaultMarketPrice, selector, url } =
|
||||||
symbolProfile.scraperConfiguration;
|
symbolProfile.scraperConfiguration;
|
||||||
|
|
||||||
@ -108,9 +107,8 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
const symbolProfiles =
|
||||||
aSymbols
|
await this.symbolProfileService.getSymbolProfilesBySymbols(aSymbols);
|
||||||
);
|
|
||||||
|
|
||||||
const marketData = await this.prismaService.marketData.findMany({
|
const marketData = await this.prismaService.marketData.findMany({
|
||||||
distinct: ['symbol'],
|
distinct: ['symbol'],
|
||||||
|
@ -91,9 +91,8 @@ export class GoogleSheetsService implements DataProviderInterface {
|
|||||||
try {
|
try {
|
||||||
const response: { [symbol: string]: IDataProviderResponse } = {};
|
const response: { [symbol: string]: IDataProviderResponse } = {};
|
||||||
|
|
||||||
const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
|
const symbolProfiles =
|
||||||
aSymbols
|
await this.symbolProfileService.getSymbolProfilesBySymbols(aSymbols);
|
||||||
);
|
|
||||||
|
|
||||||
const sheet = await this.getSheet({
|
const sheet = await this.getSheet({
|
||||||
sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'),
|
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({
|
public async getRange({
|
||||||
dateQuery,
|
dateQuery,
|
||||||
symbols
|
symbols
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
|
import {
|
||||||
|
EnhancedSymbolProfile,
|
||||||
|
ScraperConfiguration,
|
||||||
|
UniqueAsset
|
||||||
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
@ -12,8 +16,6 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { continents, countries } from 'countries-list';
|
import { continents, countries } from 'countries-list';
|
||||||
|
|
||||||
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SymbolProfileService {
|
export class SymbolProfileService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
@ -37,6 +39,35 @@ export class SymbolProfileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getSymbolProfiles(
|
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[]
|
symbols: string[]
|
||||||
): Promise<EnhancedSymbolProfile[]> {
|
): Promise<EnhancedSymbolProfile[]> {
|
||||||
return this.prismaService.symbolProfile
|
return this.prismaService.symbolProfile
|
||||||
|
@ -4,6 +4,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|||||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
|
Benchmark,
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
User
|
User
|
||||||
@ -18,6 +19,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './home-market.html'
|
templateUrl: './home-market.html'
|
||||||
})
|
})
|
||||||
export class HomeMarketComponent implements OnDestroy, OnInit {
|
export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||||
|
public benchmarks: Benchmark[];
|
||||||
public fearAndGreedIndex: number;
|
public fearAndGreedIndex: number;
|
||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public historicalData: HistoricalDataItem[];
|
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();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
<div
|
<div class="container">
|
||||||
class="align-items-center container d-flex flex-grow-1 h-100 justify-content-center w-100"
|
<h3 class="mb-3 text-center" i18n>Markets</h3>
|
||||||
>
|
<div class="mb-5 row">
|
||||||
<div class="no-gutters row w-100">
|
|
||||||
<div class="col-xs-12 col-md-8 offset-md-2">
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
<div class="mb-2 text-center text-muted">
|
<div class="mb-2 text-center text-muted">
|
||||||
<small i18n>Last {{ numberOfDays }} Days</small>
|
<small i18n>Last {{ numberOfDays }} Days</small>
|
||||||
</div>
|
</div>
|
||||||
<gf-line-chart
|
<gf-line-chart
|
||||||
class="mb-5"
|
class="mb-3"
|
||||||
yMax="100"
|
yMax="100"
|
||||||
yMaxLabel="Greed"
|
yMaxLabel="Greed"
|
||||||
yMin="0"
|
yMin="0"
|
||||||
@ -23,4 +22,20 @@
|
|||||||
></gf-fear-and-greed-index>
|
></gf-fear-and-greed-index>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
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 { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||||
|
|
||||||
import { HomeMarketComponent } from './home-market.component';
|
import { HomeMarketComponent } from './home-market.component';
|
||||||
@ -8,7 +9,12 @@ import { HomeMarketComponent } from './home-market.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [HomeMarketComponent],
|
declarations: [HomeMarketComponent],
|
||||||
exports: [],
|
exports: [],
|
||||||
imports: [CommonModule, GfFearAndGreedIndexModule, GfLineChartModule],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfBenchmarkModule,
|
||||||
|
GfFearAndGreedIndexModule,
|
||||||
|
GfLineChartModule
|
||||||
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@ -7,9 +7,9 @@ import {
|
|||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
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 { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
|
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { Tag } from '@prisma/client';
|
import { Tag } from '@prisma/client';
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
Accounts,
|
Accounts,
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
|
BenchmarkResponse,
|
||||||
Export,
|
Export,
|
||||||
Filter,
|
Filter,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
@ -90,6 +91,10 @@ export class DataService {
|
|||||||
return this.http.get<Access[]>('/api/v1/access');
|
return this.http.get<Access[]>('/api/v1/access');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchBenchmarks() {
|
||||||
|
return this.http.get<BenchmarkResponse>('/api/v1/benchmark');
|
||||||
|
}
|
||||||
|
|
||||||
public fetchChart({ range }: { range: DateRange }) {
|
public fetchChart({ range }: { range: DateRange }) {
|
||||||
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
return this.http.get<PortfolioChart>('/api/v1/portfolio/chart', {
|
||||||
params: { range }
|
params: { range }
|
||||||
|
@ -60,12 +60,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngx-skeleton-loader {
|
ngx-skeleton-loader {
|
||||||
line-height: 0;
|
|
||||||
outline: 0;
|
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
background-color: #323232;
|
background-color: #323232;
|
||||||
outline: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,9 +113,13 @@ ion-icon {
|
|||||||
|
|
||||||
ngx-skeleton-loader {
|
ngx-skeleton-loader {
|
||||||
display: block;
|
display: block;
|
||||||
|
line-height: 0;
|
||||||
|
outline: 0;
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
|
display: flex;
|
||||||
margin: 0 !important;
|
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 GATHER_ASSET_PROFILE_PROCESS = 'GATHER_ASSET_PROFILE';
|
||||||
|
|
||||||
|
export const PROPERTY_BENCHMARKS = 'BENCHMARKS';
|
||||||
export const PROPERTY_COUPONS = 'COUPONS';
|
export const PROPERTY_COUPONS = 'COUPONS';
|
||||||
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
||||||
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
|
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 { 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 {
|
export interface EnhancedSymbolProfile {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
@ -6,7 +6,9 @@ import {
|
|||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataItem
|
AdminMarketDataItem
|
||||||
} from './admin-market-data.interface';
|
} from './admin-market-data.interface';
|
||||||
|
import { Benchmark } from './benchmark.interface';
|
||||||
import { Coupon } from './coupon.interface';
|
import { Coupon } from './coupon.interface';
|
||||||
|
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||||
import { Export } from './export.interface';
|
import { Export } from './export.interface';
|
||||||
import { FilterGroup } from './filter-group.interface';
|
import { FilterGroup } from './filter-group.interface';
|
||||||
import { Filter } from './filter.interface';
|
import { Filter } from './filter.interface';
|
||||||
@ -24,8 +26,10 @@ import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
|||||||
import { PortfolioReport } from './portfolio-report.interface';
|
import { PortfolioReport } from './portfolio-report.interface';
|
||||||
import { PortfolioSummary } from './portfolio-summary.interface';
|
import { PortfolioSummary } from './portfolio-summary.interface';
|
||||||
import { Position } from './position.interface';
|
import { Position } from './position.interface';
|
||||||
|
import { BenchmarkResponse } from './responses/benchmark-response.interface';
|
||||||
import { ResponseError } from './responses/errors.interface';
|
import { ResponseError } from './responses/errors.interface';
|
||||||
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
import { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface';
|
||||||
|
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||||
import { TimelinePosition } from './timeline-position.interface';
|
import { TimelinePosition } from './timeline-position.interface';
|
||||||
import { UniqueAsset } from './unique-asset.interface';
|
import { UniqueAsset } from './unique-asset.interface';
|
||||||
import { UserSettings } from './user-settings.interface';
|
import { UserSettings } from './user-settings.interface';
|
||||||
@ -39,7 +43,10 @@ export {
|
|||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
AdminMarketDataDetails,
|
||||||
AdminMarketDataItem,
|
AdminMarketDataItem,
|
||||||
|
Benchmark,
|
||||||
|
BenchmarkResponse,
|
||||||
Coupon,
|
Coupon,
|
||||||
|
EnhancedSymbolProfile,
|
||||||
Export,
|
Export,
|
||||||
Filter,
|
Filter,
|
||||||
FilterGroup,
|
FilterGroup,
|
||||||
@ -59,6 +66,7 @@ export {
|
|||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
Position,
|
Position,
|
||||||
ResponseError,
|
ResponseError,
|
||||||
|
ScraperConfiguration,
|
||||||
TimelinePosition,
|
TimelinePosition,
|
||||||
UniqueAsset,
|
UniqueAsset,
|
||||||
User,
|
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"
|
*ngIf="value === undefined"
|
||||||
animation="pulse"
|
animation="pulse"
|
||||||
[theme]="{
|
[theme]="{
|
||||||
height: size === 'large' ? '2.5rem' : '1.5rem',
|
height: size === 'large' ? '2.5rem' : size === 'medium' ? '2rem' : '1.5rem',
|
||||||
width: '5rem'
|
width: '5rem'
|
||||||
}"
|
}"
|
||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user