Setup benchmark comparator
This commit is contained in:
parent
0de28d733e
commit
75d61bff6d
@ -56,11 +56,11 @@ export class BenchmarkService {
|
||||
const allTimeHighs = await Promise.all(promises);
|
||||
|
||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol] ?? {};
|
||||
|
||||
let performancePercentFromAllTimeHigh = new Big(0);
|
||||
|
||||
if (allTimeHigh) {
|
||||
if (allTimeHigh && marketPrice) {
|
||||
performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||
.div(allTimeHigh)
|
||||
.minus(1);
|
||||
@ -93,6 +93,24 @@ export class BenchmarkService {
|
||||
return benchmarks;
|
||||
}
|
||||
|
||||
public async getBenchmarkAssetProfiles(): Promise<UniqueAsset[]> {
|
||||
const benchmarkAssets: UniqueAsset[] =
|
||||
((await this.propertyService.getByKey(
|
||||
PROPERTY_BENCHMARKS
|
||||
)) as UniqueAsset[]) ?? [];
|
||||
|
||||
const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
|
||||
benchmarkAssets
|
||||
);
|
||||
|
||||
return assetProfiles.map(({ dataSource, symbol }) => {
|
||||
return {
|
||||
dataSource,
|
||||
symbol
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private getMarketCondition(aPerformanceInPercent: Big) {
|
||||
return aPerformanceInPercent.lte(-0.2) ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||
|
||||
import { InfoService } from './info.service';
|
||||
|
||||
@ -8,6 +9,7 @@ export class InfoController {
|
||||
public constructor(private readonly infoService: InfoService) {}
|
||||
|
||||
@Get()
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getInfo(): Promise<InfoItem> {
|
||||
return this.infoService.get();
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.mod
|
||||
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||
|
||||
import { InfoController } from './info.controller';
|
||||
import { InfoService } from './info.service';
|
||||
@ -16,6 +17,7 @@ import { InfoService } from './info.service';
|
||||
@Module({
|
||||
controllers: [InfoController],
|
||||
imports: [
|
||||
BenchmarkModule,
|
||||
ConfigurationModule,
|
||||
DataGatheringModule,
|
||||
DataProviderModule,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { BenchmarkService } from '@ghostfolio/api/app/benchmark/benchmark.service';
|
||||
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
@ -31,6 +32,7 @@ export class InfoService {
|
||||
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
||||
|
||||
public constructor(
|
||||
private readonly benchmarkService: BenchmarkService,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||
private readonly jwtService: JwtService,
|
||||
@ -108,6 +110,7 @@ export class InfoService {
|
||||
platforms,
|
||||
systemMessage,
|
||||
baseCurrency: this.configurationService.get('BASE_CURRENCY'),
|
||||
benchmarks: await this.benchmarkService.getBenchmarkAssetProfiles(),
|
||||
currencies: this.exchangeRateDataService.getCurrencies(),
|
||||
demoAuthToken: this.getDemoAuthToken(),
|
||||
statistics: await this.getStatistics(),
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
Injectable,
|
||||
NestInterceptor
|
||||
} from '@nestjs/common';
|
||||
import { isArray } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@ -36,6 +37,13 @@ export class TransformDataSourceInResponseInterceptor<T>
|
||||
});
|
||||
}
|
||||
|
||||
if (isArray(data.benchmarks)) {
|
||||
data.benchmarks.map((benchmark) => {
|
||||
benchmark.dataSource = encodeDataSource(benchmark.dataSource);
|
||||
return benchmark;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.dataSource) {
|
||||
data.dataSource = encodeDataSource(data.dataSource);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { BenchmarkModule } from '@ghostfolio/api/app/benchmark/benchmark.module';
|
||||
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module';
|
||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
|
@ -14,8 +14,8 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
||||
declarations: [AdminOverviewComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
|
@ -0,0 +1,39 @@
|
||||
<div class="align-items-center d-flex mb-4">
|
||||
<div class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate">
|
||||
<span i18n>Benchmarks</span>
|
||||
<sup i18n>Beta</sup>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Compare with...</mat-label>
|
||||
<mat-select
|
||||
name="benchmark"
|
||||
[value]="value"
|
||||
(selectionChange)="onChangeBenchmark($event.value)"
|
||||
>
|
||||
<mat-option *ngFor="let benchmark of benchmarks" [value]="benchmark">{{
|
||||
benchmark.symbol
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
[theme]="{
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
<canvas
|
||||
#chartCanvas
|
||||
class="h-100"
|
||||
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
|
||||
></canvas>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
ngx-skeleton-loader {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
import 'chartjs-adapter-date-fns';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {
|
||||
getTooltipOptions,
|
||||
getTooltipPositionerMapTop,
|
||||
getVerticalHoverLinePlugin
|
||||
} from '@ghostfolio/common/chart-helper';
|
||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||
import {
|
||||
getBackgroundColor,
|
||||
getDateFormatString,
|
||||
getTextColor,
|
||||
parseDate,
|
||||
transformTickToAbbreviation
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import {
|
||||
Chart,
|
||||
LineController,
|
||||
LineElement,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
Tooltip
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-benchmark-comparator',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './benchmark-comparator.component.html',
|
||||
styleUrls: ['./benchmark-comparator.component.scss']
|
||||
})
|
||||
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
@Input() benchmarks: UniqueAsset[];
|
||||
@Input() currency: string;
|
||||
@Input() daysInMarket: number;
|
||||
@Input() investments: InvestmentItem[];
|
||||
@Input() isInPercent = false;
|
||||
@Input() locale: string;
|
||||
@Input() user: User;
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public chart: Chart;
|
||||
public isLoading = true;
|
||||
public value;
|
||||
|
||||
private data: InvestmentItem[];
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
annotationPlugin,
|
||||
LinearScale,
|
||||
LineController,
|
||||
LineElement,
|
||||
PointElement,
|
||||
TimeScale,
|
||||
Tooltip
|
||||
);
|
||||
|
||||
Tooltip.positioners['top'] = (elements, position) =>
|
||||
getTooltipPositionerMapTop(this.chart, position);
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
if (this.investments) {
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
public onChangeBenchmark(aBenchmark: any) {
|
||||
console.log(aBenchmark);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.chart?.destroy();
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.isLoading = true;
|
||||
|
||||
// Create a clone
|
||||
this.data = this.investments.map((a) => Object.assign({}, a));
|
||||
|
||||
if (this.data?.length > 0) {
|
||||
// Extend chart by 5% of days in market (before)
|
||||
const firstItem = this.data[0];
|
||||
this.data.unshift({
|
||||
...firstItem,
|
||||
date: subDays(
|
||||
parseISO(firstItem.date),
|
||||
this.daysInMarket * 0.05 || 90
|
||||
).toISOString(),
|
||||
investment: 0
|
||||
});
|
||||
|
||||
// Extend chart by 5% of days in market (after)
|
||||
const lastItem = this.data[this.data.length - 1];
|
||||
this.data.push({
|
||||
...lastItem,
|
||||
date: addDays(
|
||||
parseDate(lastItem.date),
|
||||
this.daysInMarket * 0.05 || 90
|
||||
).toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: this.data.map((investmentItem) => {
|
||||
return investmentItem.date;
|
||||
}),
|
||||
datasets: [
|
||||
{
|
||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderWidth: 2,
|
||||
data: this.data.map((position) => {
|
||||
return position.investment;
|
||||
}),
|
||||
label: $localize`Deposit`,
|
||||
segment: {
|
||||
borderColor: (context: unknown) =>
|
||||
this.isInFuture(
|
||||
context,
|
||||
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)`
|
||||
),
|
||||
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
|
||||
},
|
||||
stepped: true
|
||||
},
|
||||
{
|
||||
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||
borderWidth: 2,
|
||||
data: this.data.map((position) => {
|
||||
return position.investment * 1.75;
|
||||
}),
|
||||
label: $localize`Benchmark`
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (this.chartCanvas) {
|
||||
if (this.chart) {
|
||||
this.chart.data = data;
|
||||
this.chart.options.plugins.tooltip = <unknown>(
|
||||
this.getTooltipPluginConfiguration()
|
||||
);
|
||||
this.chart.update();
|
||||
} else {
|
||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||
data,
|
||||
options: {
|
||||
animation: false,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
},
|
||||
point: {
|
||||
hoverBackgroundColor: getBackgroundColor(),
|
||||
hoverRadius: 2,
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
interaction: { intersect: false, mode: 'index' },
|
||||
maintainAspectRatio: true,
|
||||
plugins: <unknown>{
|
||||
annotation: {
|
||||
annotations: {
|
||||
yAxis: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
borderWidth: 1,
|
||||
scaleID: 'y',
|
||||
type: 'line',
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: this.getTooltipPluginConfiguration(),
|
||||
verticalHoverLine: {
|
||||
color: `rgba(${getTextColor()}, 0.1)`
|
||||
}
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
borderWidth: 1,
|
||||
color: `rgba(${getTextColor()}, 0.8)`,
|
||||
display: false
|
||||
},
|
||||
type: 'time',
|
||||
time: {
|
||||
tooltipFormat: getDateFormatString(this.locale),
|
||||
unit: 'year'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: !this.isInPercent,
|
||||
grid: {
|
||||
borderColor: `rgba(${getTextColor()}, 0.1)`,
|
||||
color: `rgba(${getTextColor()}, 0.8)`,
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
position: 'right',
|
||||
ticks: {
|
||||
callback: (value: number) => {
|
||||
return transformTickToAbbreviation(value);
|
||||
},
|
||||
display: true,
|
||||
mirror: true,
|
||||
z: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
|
||||
type: 'line'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private getTooltipPluginConfiguration() {
|
||||
return {
|
||||
...getTooltipOptions({
|
||||
locale: this.isInPercent ? undefined : this.locale,
|
||||
unit: this.isInPercent ? undefined : this.currency
|
||||
}),
|
||||
mode: 'index',
|
||||
position: <unknown>'top',
|
||||
xAlign: 'center',
|
||||
yAlign: 'bottom'
|
||||
};
|
||||
}
|
||||
|
||||
private isInFuture<T>(aContext: any, aValue: T) {
|
||||
return isAfter(new Date(aContext?.p1?.parsed?.x), new Date())
|
||||
? aValue
|
||||
: undefined;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { BenchmarkComparatorComponent } from './benchmark-comparator.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [BenchmarkComparatorComponent],
|
||||
exports: [BenchmarkComparatorComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatSelectModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class GfBenchmarkComparatorModule {}
|
@ -57,6 +57,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
public chart: Chart;
|
||||
public isLoading = true;
|
||||
|
||||
private data: InvestmentItem[];
|
||||
|
||||
public constructor() {
|
||||
Chart.register(
|
||||
annotationPlugin,
|
||||
@ -87,10 +89,13 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
private initialize() {
|
||||
this.isLoading = true;
|
||||
|
||||
if (!this.groupBy && this.investments?.length > 0) {
|
||||
// Create a clone
|
||||
this.data = this.investments.map((a) => Object.assign({}, a));
|
||||
|
||||
if (!this.groupBy && this.data?.length > 0) {
|
||||
// Extend chart by 5% of days in market (before)
|
||||
const firstItem = this.investments[0];
|
||||
this.investments.unshift({
|
||||
const firstItem = this.data[0];
|
||||
this.data.unshift({
|
||||
...firstItem,
|
||||
date: subDays(
|
||||
parseISO(firstItem.date),
|
||||
@ -100,8 +105,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
|
||||
// Extend chart by 5% of days in market (after)
|
||||
const lastItem = this.investments[this.investments.length - 1];
|
||||
this.investments.push({
|
||||
const lastItem = this.data[this.data.length - 1];
|
||||
this.data.push({
|
||||
...lastItem,
|
||||
date: addDays(
|
||||
parseDate(lastItem.date),
|
||||
@ -111,7 +116,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
const data = {
|
||||
labels: this.investments.map((investmentItem) => {
|
||||
labels: this.data.map((investmentItem) => {
|
||||
return investmentItem.date;
|
||||
}),
|
||||
datasets: [
|
||||
@ -119,7 +124,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||
borderWidth: this.groupBy ? 0 : 2,
|
||||
data: this.investments.map((position) => {
|
||||
data: this.data.map((position) => {
|
||||
return position.investment;
|
||||
}),
|
||||
label: $localize`Deposit`,
|
||||
|
@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||
import { Position, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||
import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
@ -18,6 +18,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './analysis-page.html'
|
||||
})
|
||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public benchmarks: UniqueAsset[];
|
||||
public bottom3: Position[];
|
||||
public daysInMarket: number;
|
||||
public deviceType: string;
|
||||
@ -40,7 +41,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private userService: UserService
|
||||
) {}
|
||||
) {
|
||||
const { benchmarks } = this.dataService.fetchInfo();
|
||||
this.benchmarks = benchmarks;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
@ -1,53 +1,21 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||
<div *ngIf="user?.settings?.isExperimentalFeatures" class="mb-5 row">
|
||||
<div class="col-lg">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
|
||||
<div class="mb-4">
|
||||
<div class="align-items-center d-flex mb-4">
|
||||
<div
|
||||
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
|
||||
>
|
||||
<span i18n>Investment Timeline</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
[defaultValue]="mode"
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investments"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': mode }"
|
||||
></gf-investment-chart>
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
groupBy="month"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investmentsByMonth"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': !mode }"
|
||||
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
||||
></gf-investment-chart>
|
||||
</div>
|
||||
</div>
|
||||
<gf-benchmark-comparator
|
||||
class="h-100"
|
||||
[benchmarks]="benchmarks"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investments"
|
||||
[locale]="user?.settings?.locale"
|
||||
[user]="user"
|
||||
></gf-benchmark-comparator>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-5 row">
|
||||
<div class="col-md-6">
|
||||
<mat-card class="mb-3">
|
||||
<mat-card-header>
|
||||
@ -124,4 +92,49 @@
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg">
|
||||
<div class="align-items-center d-flex mb-4">
|
||||
<div
|
||||
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
|
||||
>
|
||||
<span i18n>Investment Timeline</span>
|
||||
<gf-premium-indicator
|
||||
*ngIf="user?.subscription?.type === 'Basic'"
|
||||
class="ml-1"
|
||||
></gf-premium-indicator>
|
||||
</div>
|
||||
<gf-toggle
|
||||
class="d-none d-lg-block"
|
||||
[defaultValue]="mode"
|
||||
[isLoading]="false"
|
||||
[options]="modeOptions"
|
||||
(change)="onChangeGroupBy($event.value)"
|
||||
></gf-toggle>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investments"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': mode }"
|
||||
></gf-investment-chart>
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
groupBy="month"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[investments]="investmentsByMonth"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'd-none': !mode }"
|
||||
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
||||
></gf-investment-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfBenchmarkComparatorModule } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.module';
|
||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
|
||||
@ -15,6 +16,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
||||
imports: [
|
||||
AnalysisPageRoutingModule,
|
||||
CommonModule,
|
||||
GfBenchmarkComparatorModule,
|
||||
GfInvestmentChartModule,
|
||||
GfPremiumIndicatorModule,
|
||||
GfToggleModule,
|
||||
|
@ -42,7 +42,11 @@ export function downloadAsFile({
|
||||
}
|
||||
|
||||
export function encodeDataSource(aDataSource: DataSource) {
|
||||
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
||||
if (aDataSource) {
|
||||
return Buffer.from(aDataSource, 'utf-8').toString('hex');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function extractNumberFromString(aString: string): number {
|
||||
|
@ -2,9 +2,11 @@ import { Tag } from '@prisma/client';
|
||||
|
||||
import { Statistics } from './statistics.interface';
|
||||
import { Subscription } from './subscription.interface';
|
||||
import { UniqueAsset } from './unique-asset.interface';
|
||||
|
||||
export interface InfoItem {
|
||||
baseCurrency: string;
|
||||
benchmarks: UniqueAsset[];
|
||||
currencies: string[];
|
||||
demoAuthToken: string;
|
||||
fearAndGreedDataSource?: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user