From aa72287d54e6587ed08887ff687d0ae750d44e4f Mon Sep 17 00:00:00 2001 From: Aldrin <53973174+Dhoni77@users.noreply.github.com> Date: Thu, 16 Nov 2023 00:55:16 +0530 Subject: [PATCH] Extend benchmarks in the markets overview by 50-Day and 200-Day trends (#2575) * Extend benchmarks in the markets overview by 50-Day and 200-Day trends * Update changelog --------- Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 5 +- .../src/app/benchmark/benchmark.service.ts | 58 ++++++++++++++++--- .../market-data/market-data.service.ts | 4 +- .../components/home-market/home-market.html | 1 + .../position/position.component.html | 1 + libs/common/src/lib/helper.ts | 57 +++++++++++++++++- .../src/lib/interfaces/benchmark.interface.ts | 4 ++ .../src/lib/types/benchmark-trend.type.ts | 1 + libs/common/src/lib/types/index.ts | 2 + .../lib/benchmark/benchmark.component.html | 53 ++++++++++++++++- .../src/lib/benchmark/benchmark.component.ts | 15 +++-- libs/ui/src/lib/benchmark/benchmark.module.ts | 2 + .../trend-indicator.component.html | 10 ++-- .../trend-indicator.component.ts | 1 + 14 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 libs/common/src/lib/types/benchmark-trend.type.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 559748b1a..9dd09672b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Extended the benchmarks in the markets overview by 50-Day and 200-Day trends (experimental) - Set up the language localization for Polski (`pl`) ### Changed @@ -197,7 +198,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support to transfer a part of the cash balance from one to another account -- Extended the markets overview by benchmarks (date of last all time high) +- Extended the benchmarks in the markets overview by the date of the last all time high - Added support to import historical market data in the admin control panel ### Changed @@ -2437,7 +2438,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) +- Extended the benchmarks in the markets overview by the current change to the all time high ## 1.151.0 - 24.05.2022 diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 2547e57cc..1f143fa2e 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -9,17 +9,21 @@ import { MAX_CHART_ITEMS, PROPERTY_BENCHMARKS } from '@ghostfolio/common/config'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { + DATE_FORMAT, + calculateBenchmarkTrend +} from '@ghostfolio/common/helper'; import { BenchmarkMarketDataDetails, BenchmarkProperty, BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces'; +import { BenchmarkTrend } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import Big from 'big.js'; -import { format } from 'date-fns'; +import { format, subDays } from 'date-fns'; import { uniqBy } from 'lodash'; import ms from 'ms'; @@ -45,6 +49,30 @@ export class BenchmarkService { return 0; } + public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) { + const historicalData = await this.marketDataService.marketDataItems({ + orderBy: { + date: 'desc' + }, + where: { + dataSource, + symbol, + date: { gte: subDays(new Date(), 400) } + } + }); + + const fiftyDayAverage = calculateBenchmarkTrend({ + historicalData, + days: 50 + }); + const twoHundredDayAverage = calculateBenchmarkTrend({ + historicalData, + days: 200 + }); + + return { trend50d: fiftyDayAverage, trend200d: twoHundredDayAverage }; + } + public async getBenchmarks({ useCache = true } = {}): Promise< BenchmarkResponse['benchmarks'] > { @@ -64,7 +92,12 @@ export class BenchmarkService { const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles(); - const promises: Promise<{ date: Date; marketPrice: number }>[] = []; + const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] = + []; + const promisesBenchmarkTrends: Promise<{ + trend50d: BenchmarkTrend; + trend200d: BenchmarkTrend; + }>[] = []; const quotes = await this.dataProviderService.getQuotes({ items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => { @@ -73,10 +106,18 @@ export class BenchmarkService { }); for (const { dataSource, symbol } of benchmarkAssetProfiles) { - promises.push(this.marketDataService.getMax({ dataSource, symbol })); + promisesAllTimeHighs.push( + this.marketDataService.getMax({ dataSource, symbol }) + ); + promisesBenchmarkTrends.push( + this.getBenchmarkTrends({ dataSource, symbol }) + ); } - const allTimeHighs = await Promise.all(promises); + const [allTimeHighs, benchmarkTrends] = await Promise.all([ + Promise.all(promisesAllTimeHighs), + Promise.all(promisesBenchmarkTrends) + ]); let storeInCache = true; benchmarks = allTimeHighs.map((allTimeHigh, index) => { @@ -93,6 +134,7 @@ export class BenchmarkService { } else { storeInCache = false; } + return { marketCondition: this.getMarketCondition( performancePercentFromAllTimeHigh @@ -100,10 +142,12 @@ export class BenchmarkService { name: benchmarkAssetProfiles[index].name, performances: { allTimeHigh: { - date: allTimeHigh.date, + date: allTimeHigh?.date, performancePercent: performancePercentFromAllTimeHigh } - } + }, + trend50d: benchmarkTrends[index].trend50d, + trend200d: benchmarkTrends[index].trend200d }; }); diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index 52c833784..01f8bb9aa 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -90,15 +90,17 @@ export class MarketDataService { } public async marketDataItems(params: { + select?: Prisma.MarketDataSelectScalar; skip?: number; take?: number; cursor?: Prisma.MarketDataWhereUniqueInput; where?: Prisma.MarketDataWhereInput; orderBy?: Prisma.MarketDataOrderByWithRelationInput; }): Promise { - const { skip, take, cursor, where, orderBy } = params; + const { select, skip, take, cursor, where, orderBy } = params; return this.prismaService.marketData.findMany({ + select, cursor, orderBy, skip, diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index 7ce07b6e9..46c8f1d59 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -31,6 +31,7 @@ = 2 * days; + + if (!hasEnoughData) { + return 'UNKNOWN'; + } + + const recentPeriodAverage = calculateMovingAverage({ + days, + prices: historicalData.slice(0, days).map(({ marketPrice }) => { + return new Big(marketPrice); + }) + }); + + const pastPeriodAverage = calculateMovingAverage({ + days, + prices: historicalData.slice(days, 2 * days).map(({ marketPrice }) => { + return new Big(marketPrice); + }) + }); + + if (recentPeriodAverage > pastPeriodAverage) { + return 'UP'; + } + + if (recentPeriodAverage < pastPeriodAverage) { + return 'DOWN'; + } + + return 'NEUTRAL'; +} + +export function calculateMovingAverage({ + days, + prices +}: { + days: number; + prices: Big[]; +}) { + return prices + .reduce((previous, current) => { + return previous.add(current); + }, new Big(0)) + .div(days) + .toNumber(); +} + export function capitalize(aString: string) { return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); } diff --git a/libs/common/src/lib/interfaces/benchmark.interface.ts b/libs/common/src/lib/interfaces/benchmark.interface.ts index d1a63e1f4..2124173fd 100644 --- a/libs/common/src/lib/interfaces/benchmark.interface.ts +++ b/libs/common/src/lib/interfaces/benchmark.interface.ts @@ -1,3 +1,5 @@ +import { BenchmarkTrend } from '@ghostfolio/common/types/'; + import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; export interface Benchmark { @@ -9,4 +11,6 @@ export interface Benchmark { performancePercent: number; }; }; + trend50d: BenchmarkTrend; + trend200d: BenchmarkTrend; } diff --git a/libs/common/src/lib/types/benchmark-trend.type.ts b/libs/common/src/lib/types/benchmark-trend.type.ts new file mode 100644 index 000000000..b437d388a --- /dev/null +++ b/libs/common/src/lib/types/benchmark-trend.type.ts @@ -0,0 +1 @@ +export type BenchmarkTrend = 'DOWN' | 'NEUTRAL' | 'UNKNOWN' | 'UP'; diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 2af65d404..e99bd50b6 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -1,6 +1,7 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import type { AccountWithPlatform } from './account-with-platform.type'; import type { AccountWithValue } from './account-with-value.type'; +import type { BenchmarkTrend } from './benchmark-trend.type'; import type { ColorScheme } from './color-scheme.type'; import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; @@ -20,6 +21,7 @@ export type { AccessWithGranteeUser, AccountWithPlatform, AccountWithValue, + BenchmarkTrend, ColorScheme, DateRange, Granularity, diff --git a/libs/ui/src/lib/benchmark/benchmark.component.html b/libs/ui/src/lib/benchmark/benchmark.component.html index 33cf72389..39e1db7c1 100644 --- a/libs/ui/src/lib/benchmark/benchmark.component.html +++ b/libs/ui/src/lib/benchmark/benchmark.component.html @@ -6,6 +6,54 @@ + + + 50-Day Trend + + +
+ +
+ +
+ + + + 200-Day Trend + + +
+ +
+ +
+ + /> @@ -35,7 +83,6 @@ + /> diff --git a/libs/ui/src/lib/benchmark/benchmark.component.ts b/libs/ui/src/lib/benchmark/benchmark.component.ts index b9f1dd25b..215cc15c6 100644 --- a/libs/ui/src/lib/benchmark/benchmark.component.ts +++ b/libs/ui/src/lib/benchmark/benchmark.component.ts @@ -4,9 +4,8 @@ import { Input, OnChanges } from '@angular/core'; -import { locale } from '@ghostfolio/common/config'; import { resolveMarketCondition } from '@ghostfolio/common/helper'; -import { Benchmark } from '@ghostfolio/common/interfaces'; +import { Benchmark, User } from '@ghostfolio/common/interfaces'; @Component({ selector: 'gf-benchmark', @@ -17,6 +16,7 @@ import { Benchmark } from '@ghostfolio/common/interfaces'; export class BenchmarkComponent implements OnChanges { @Input() benchmarks: Benchmark[]; @Input() locale: string; + @Input() user: User; public displayedColumns = ['name', 'date', 'change', 'marketCondition']; public resolveMarketCondition = resolveMarketCondition; @@ -24,8 +24,15 @@ export class BenchmarkComponent implements OnChanges { public constructor() {} public ngOnChanges() { - if (!this.locale) { - this.locale = locale; + if (this.user?.settings?.isExperimentalFeatures) { + this.displayedColumns = [ + 'name', + 'trend50d', + 'trend200d', + 'date', + 'change', + 'marketCondition' + ]; } } } diff --git a/libs/ui/src/lib/benchmark/benchmark.module.ts b/libs/ui/src/lib/benchmark/benchmark.module.ts index 1768aa39f..5b3e00209 100644 --- a/libs/ui/src/lib/benchmark/benchmark.module.ts +++ b/libs/ui/src/lib/benchmark/benchmark.module.ts @@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { MatTableModule } from '@angular/material/table'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { GfTrendIndicatorModule } from '../trend-indicator'; import { GfValueModule } from '../value'; import { BenchmarkComponent } from './benchmark.component'; @@ -11,6 +12,7 @@ import { BenchmarkComponent } from './benchmark.component'; exports: [BenchmarkComponent], imports: [ CommonModule, + GfTrendIndicatorModule, GfValueModule, MatTableModule, NgxSkeletonLoaderModule diff --git a/libs/ui/src/lib/trend-indicator/trend-indicator.component.html b/libs/ui/src/lib/trend-indicator/trend-indicator.component.html index 27251fc24..d6180cba7 100644 --- a/libs/ui/src/lib/trend-indicator/trend-indicator.component.html +++ b/libs/ui/src/lib/trend-indicator/trend-indicator.component.html @@ -13,7 +13,7 @@ *ngIf="marketState === 'closed' && range === '1d'; else delayed" class="text-muted" name="pause-circle-outline" - size="large" + [size]="size" > @@ -21,7 +21,7 @@ *ngIf="marketState === 'delayed' && range === '1d'; else trend" class="text-muted" name="time-outline" - size="large" + [size]="size" > @@ -31,21 +31,21 @@ *ngIf="value <= -0.0005" class="text-danger" name="arrow-down-circle-outline" - size="large" [ngClass]="{ 'rotate-45-down': value > -0.01 }" + [size]="size" > diff --git a/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts b/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts index 4da6d6c8e..e9152f8a0 100644 --- a/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts +++ b/libs/ui/src/lib/trend-indicator/trend-indicator.component.ts @@ -11,6 +11,7 @@ export class TrendIndicatorComponent { @Input() isLoading = false; @Input() marketState: MarketState = 'open'; @Input() range: DateRange = 'max'; + @Input() size: 'large' | 'medium' | 'small' = 'small'; @Input() value = 0; public constructor() {}