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>
This commit is contained in:
parent
d155ab6f28
commit
aa72287d54
@ -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
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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<MarketData[]> {
|
||||
const { skip, take, cursor, where, orderBy } = params;
|
||||
const { select, skip, take, cursor, where, orderBy } = params;
|
||||
|
||||
return this.prismaService.marketData.findMany({
|
||||
select,
|
||||
cursor,
|
||||
orderBy,
|
||||
skip,
|
||||
|
@ -31,6 +31,7 @@
|
||||
<gf-benchmark
|
||||
[benchmarks]="benchmarks"
|
||||
[locale]="user?.settings?.locale"
|
||||
[user]="user"
|
||||
></gf-benchmark>
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
|
@ -13,6 +13,7 @@
|
||||
<div class="d-flex mr-2">
|
||||
<gf-trend-indicator
|
||||
class="d-flex"
|
||||
size="large"
|
||||
[isLoading]="isLoading"
|
||||
[marketState]="position?.marketState"
|
||||
[range]="range"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as currencies from '@dinero.js/currencies';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import {
|
||||
getDate,
|
||||
@ -14,7 +14,7 @@ import { de, es, fr, it, nl, pl, pt, tr } from 'date-fns/locale';
|
||||
|
||||
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
|
||||
import { Benchmark, UniqueAsset } from './interfaces';
|
||||
import { ColorScheme } from './types';
|
||||
import { BenchmarkTrend, ColorScheme } from './types';
|
||||
|
||||
export const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
|
||||
@ -22,6 +22,59 @@ export const DATE_FORMAT_YEARLY = 'yyyy';
|
||||
|
||||
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
|
||||
|
||||
export function calculateBenchmarkTrend({
|
||||
days,
|
||||
historicalData
|
||||
}: {
|
||||
days: number;
|
||||
historicalData: MarketData[];
|
||||
}): BenchmarkTrend {
|
||||
const hasEnoughData = historicalData.length >= 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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
1
libs/common/src/lib/types/benchmark-trend.type.ts
Normal file
1
libs/common/src/lib/types/benchmark-trend.type.ts
Normal file
@ -0,0 +1 @@
|
||||
export type BenchmarkTrend = 'DOWN' | 'NEUTRAL' | 'UNKNOWN' | 'UP';
|
@ -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,
|
||||
|
@ -6,6 +6,54 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="trend50d">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-2 text-right"
|
||||
mat-header-cell
|
||||
>
|
||||
<ng-container i18n>50-Day Trend</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-trend-indicator
|
||||
*ngIf="element?.trend50d !== 'UNKNOWN'"
|
||||
[value]="
|
||||
element?.trend50d === 'UP'
|
||||
? 0.001
|
||||
: element?.trend50d === 'DOWN'
|
||||
? -0.001
|
||||
: 0
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="trend200d">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-2 text-right"
|
||||
mat-header-cell
|
||||
>
|
||||
<ng-container i18n>200-Day Trend</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-trend-indicator
|
||||
*ngIf="element?.trend200d !== 'UNKNOWN'"
|
||||
[value]="
|
||||
element?.trend200d === 'UP'
|
||||
? 0.001
|
||||
: element?.trend200d === 'DOWN'
|
||||
? -0.001
|
||||
: 0
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="date">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
@ -20,7 +68,7 @@
|
||||
[isDate]="true"
|
||||
[locale]="locale"
|
||||
[value]="element?.performances?.allTimeHigh?.date"
|
||||
></gf-value>
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@ -35,7 +83,6 @@
|
||||
<td *matCellDef="let element" class="px-2 text-right" mat-cell>
|
||||
<gf-value
|
||||
class="d-inline-block justify-content-end"
|
||||
size="medium"
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[ngClass]="{
|
||||
@ -45,7 +92,7 @@
|
||||
element?.performances?.allTimeHigh?.performancePercent > 0
|
||||
}"
|
||||
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
||||
></gf-value>
|
||||
/>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
@ -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'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -13,7 +13,7 @@
|
||||
*ngIf="marketState === 'closed' && range === '1d'; else delayed"
|
||||
class="text-muted"
|
||||
name="pause-circle-outline"
|
||||
size="large"
|
||||
[size]="size"
|
||||
>
|
||||
</ion-icon>
|
||||
<ng-template #delayed>
|
||||
@ -21,7 +21,7 @@
|
||||
*ngIf="marketState === 'delayed' && range === '1d'; else trend"
|
||||
class="text-muted"
|
||||
name="time-outline"
|
||||
size="large"
|
||||
[size]="size"
|
||||
>
|
||||
</ion-icon>
|
||||
</ng-template>
|
||||
@ -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"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="value > -0.0005 && value < 0.0005"
|
||||
class="text-muted"
|
||||
name="arrow-forward-circle-outline"
|
||||
size="large"
|
||||
[size]="size"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="value >= 0.0005"
|
||||
class="text-success"
|
||||
name="arrow-up-circle-outline"
|
||||
size="large"
|
||||
[ngClass]="{ 'rotate-45-up': value < 0.01 }"
|
||||
[size]="size"
|
||||
></ion-icon>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
@ -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() {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user