From 98957d282e378312b09c50f933b991a43d07d873 Mon Sep 17 00:00:00 2001 From: Matej Gerek Date: Wed, 9 Oct 2024 21:04:37 +0200 Subject: [PATCH 01/12] Feature/move tags from info to user service (#3859) * Move tags from info to user service * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/info/info.module.ts | 2 - apps/api/src/app/info/info.service.ts | 9 +---- apps/api/src/app/user/user.service.ts | 10 ++--- apps/api/src/services/tag/tag.service.ts | 40 ++++++++++++------- .../holding-detail-dialog.component.ts | 17 ++++---- ...ate-or-update-activity-dialog.component.ts | 16 ++++---- .../src/lib/interfaces/info-item.interface.ts | 3 +- .../src/lib/interfaces/user.interface.ts | 2 +- .../src/lib/assistant/assistant.component.ts | 27 +++++++++---- 10 files changed, 71 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a43aa44c..752fc2397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Moved the tags from the info to the user service - Switched the `prefer-const` rule from `warn` to `error` in the `eslint` configuration ## 2.113.0 - 2024-10-06 diff --git a/apps/api/src/app/info/info.module.ts b/apps/api/src/app/info/info.module.ts index 9b7854160..7903ac397 100644 --- a/apps/api/src/app/info/info.module.ts +++ b/apps/api/src/app/info/info.module.ts @@ -9,7 +9,6 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; -import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; @@ -33,7 +32,6 @@ import { InfoService } from './info.service'; PropertyModule, RedisCacheModule, SymbolProfileModule, - TagModule, TransformDataSourceInResponseModule, UserModule ], diff --git a/apps/api/src/app/info/info.service.ts b/apps/api/src/app/info/info.service.ts index ee225e769..bd291c511 100644 --- a/apps/api/src/app/info/info.service.ts +++ b/apps/api/src/app/info/info.service.ts @@ -5,7 +5,6 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; -import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { DEFAULT_CURRENCY, PROPERTY_BETTER_UPTIME_MONITOR_ID, @@ -47,7 +46,6 @@ export class InfoService { private readonly platformService: PlatformService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, - private readonly tagService: TagService, private readonly userService: UserService ) {} @@ -103,8 +101,7 @@ export class InfoService { isUserSignupEnabled, platforms, statistics, - subscriptions, - tags + subscriptions ] = await Promise.all([ this.benchmarkService.getBenchmarkAssetProfiles(), this.getDemoAuthToken(), @@ -113,8 +110,7 @@ export class InfoService { orderBy: { name: 'asc' } }), this.getStatistics(), - this.getSubscriptions(), - this.tagService.getPublic() + this.getSubscriptions() ]); if (isUserSignupEnabled) { @@ -130,7 +126,6 @@ export class InfoService { platforms, statistics, subscriptions, - tags, baseCurrency: DEFAULT_CURRENCY, currencies: this.exchangeRateDataService.getCurrencies() }; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index c04af5fbf..0f76b9540 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -56,7 +56,7 @@ export class UserService { { Account, id, permissions, Settings, subscription }: UserWithSettings, aLocale = locale ): Promise { - const accessesResult = await Promise.all([ + const userData = await Promise.all([ this.prismaService.access.findMany({ include: { User: true @@ -70,11 +70,11 @@ export class UserService { }, where: { userId: id } }), - this.tagService.getInUseByUser(id) + this.tagService.getTagsForUser(id) ]); - const access = accessesResult[0]; - const firstActivity = accessesResult[1]; - let tags = accessesResult[2]; + const access = userData[0]; + const firstActivity = userData[1]; + let tags = userData[2]; let systemMessage: SystemMessage; diff --git a/apps/api/src/services/tag/tag.service.ts b/apps/api/src/services/tag/tag.service.ts index 5426a0582..b16f22fbb 100644 --- a/apps/api/src/services/tag/tag.service.ts +++ b/apps/api/src/services/tag/tag.service.ts @@ -6,29 +6,39 @@ import { Injectable } from '@nestjs/common'; export class TagService { public constructor(private readonly prismaService: PrismaService) {} - public async getPublic() { - return this.prismaService.tag.findMany({ - orderBy: { - name: 'asc' + public async getTagsForUser(userId: string) { + const tags = await this.prismaService.tag.findMany({ + include: { + _count: { + select: { + orders: { + where: { + userId + } + } + } + } }, - where: { - userId: null - } - }); - } - - public async getInUseByUser(userId: string) { - return this.prismaService.tag.findMany({ orderBy: { name: 'asc' }, where: { - orders: { - some: { + OR: [ + { userId + }, + { + userId: null } - } + ] } }); + + return tags.map(({ _count, id, name, userId }) => ({ + id, + name, + userId, + isUsed: _count.orders > 0 + })); } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index b94ba79d5..792ec6f9c 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -147,8 +147,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { ) {} public ngOnInit() { - const { tags } = this.dataService.fetchInfo(); - this.activityForm = this.formBuilder.group({ tags: [] }); @@ -158,13 +156,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { { id: this.data.symbol, type: 'SYMBOL' } ]; - this.tagsAvailable = tags.map((tag) => { - return { - ...tag, - name: translate(tag.name) - }; - }); - this.activityForm .get('tags') .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) @@ -434,6 +425,14 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; + this.tagsAvailable = + this.user?.tags?.map((tag) => { + return { + ...tag, + name: translate(tag.name) + }; + }) ?? []; + this.changeDetectorRef.markForCheck(); } }); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 4690ab6a8..cb21c255d 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -76,17 +76,19 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { this.locale = this.data.user?.settings?.locale; this.dateAdapter.setLocale(this.locale); - const { currencies, platforms, tags } = this.dataService.fetchInfo(); + const { currencies, platforms } = this.dataService.fetchInfo(); this.currencies = currencies; this.defaultDateFormat = getDateFormatString(this.locale); this.platforms = platforms; - this.tagsAvailable = tags.map((tag) => { - return { - ...tag, - name: translate(tag.name) - }; - }); + + this.tagsAvailable = + this.data.user?.tags?.map((tag) => { + return { + ...tag, + name: translate(tag.name) + }; + }) ?? []; Object.keys(Type).forEach((type) => { this.typesTranslationMap[Type[type]] = translate(Type[type]); diff --git a/libs/common/src/lib/interfaces/info-item.interface.ts b/libs/common/src/lib/interfaces/info-item.interface.ts index d279c74a4..1b3926331 100644 --- a/libs/common/src/lib/interfaces/info-item.interface.ts +++ b/libs/common/src/lib/interfaces/info-item.interface.ts @@ -1,6 +1,6 @@ import { SubscriptionOffer } from '@ghostfolio/common/types'; -import { Platform, SymbolProfile, Tag } from '@prisma/client'; +import { Platform, SymbolProfile } from '@prisma/client'; import { Statistics } from './statistics.interface'; import { Subscription } from './subscription.interface'; @@ -19,5 +19,4 @@ export interface InfoItem { statistics: Statistics; stripePublicKey?: string; subscriptions: { [offer in SubscriptionOffer]: Subscription }; - tags: Tag[]; } diff --git a/libs/common/src/lib/interfaces/user.interface.ts b/libs/common/src/lib/interfaces/user.interface.ts index 2891314a0..27cd1a610 100644 --- a/libs/common/src/lib/interfaces/user.interface.ts +++ b/libs/common/src/lib/interfaces/user.interface.ts @@ -23,5 +23,5 @@ export interface User { offer: SubscriptionOffer; type: SubscriptionType; }; - tags: Tag[]; + tags: (Tag & { isUsed: boolean })[]; } diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index c60e93d88..c93d04303 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -156,7 +156,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { ) {} public ngOnInit() { - this.accounts = this.user?.accounts; this.assetClasses = Object.keys(AssetClass).map((assetClass) => { return { id: assetClass, @@ -164,13 +163,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { type: 'ASSET_CLASS' }; }); - this.tags = this.user?.tags.map(({ id, name }) => { - return { - id, - label: translate(name), - type: 'TAG' - }; - }); this.searchFormControl.valueChanges .pipe( @@ -212,6 +204,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } public ngOnChanges() { + this.accounts = this.user?.accounts ?? []; + this.dateRangeOptions = [ { label: $localize`Today`, value: '1d' }, { @@ -279,6 +273,23 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { emitEvent: false } ); + + this.tags = + this.user?.tags + ?.filter(({ isUsed }) => { + return isUsed; + }) + .map(({ id, name }) => { + return { + id, + label: translate(name), + type: 'TAG' + }; + }) ?? []; + + if (this.tags.length === 0) { + this.filterForm.get('tag').disable({ emitEvent: false }); + } } public hasFilter(aFormValue: { [key: string]: string }) { From 73cd2f9cb7192564f8ff2bbd60b1f2737f614090 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 10 Oct 2024 20:27:33 +0200 Subject: [PATCH 02/12] Bugfix/fix exception in portfolio details endpoint (#3896) * Fix exception caused by markets and marketsAdvanced * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/app/endpoints/public/public.controller.ts | 2 +- apps/api/src/app/portfolio/portfolio.controller.ts | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 752fc2397..58dd3fdbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Moved the tags from the info to the user service - Switched the `prefer-const` rule from `warn` to `error` in the `eslint` configuration +### Fixed + +- Fixed an exception in the portfolio details endpoint caused by a calculation of the allocations by market + ## 2.113.0 - 2024-10-06 ### Added diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index 9399f97bf..7488e4201 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -76,7 +76,7 @@ export class PublicController { }) ]); - Object.values(markets).forEach((market) => { + Object.values(markets ?? {}).forEach((market) => { delete market.valueInBaseCurrency; }); diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index ff0c31060..326dda151 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -172,10 +172,10 @@ export class PortfolioController { }) || isRestrictedView(this.request.user) ) { - Object.values(markets).forEach((market) => { + Object.values(markets ?? {}).forEach((market) => { delete market.valueInBaseCurrency; }); - Object.values(marketsAdvanced).forEach((market) => { + Object.values(marketsAdvanced ?? {}).forEach((market) => { delete market.valueInBaseCurrency; }); From 7252a54ae059889d5fbb3adb4d744fff9fa6c859 Mon Sep 17 00:00:00 2001 From: Uday R <82779467+uday-rana@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:06:17 -0400 Subject: [PATCH 03/12] Feature/Set up tooltip in treemap chart component (#3897) * Set up tooltip in treemap chart component * Update changelog --- CHANGELOG.md | 1 + .../home-holdings/home-holdings.html | 3 ++ .../treemap-chart/treemap-chart.component.ts | 48 ++++++++++++++++--- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58dd3fdbc..0d6ba5d26 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 +- Added a tooltip to the chart of the holdings tab on the home page (experimental) - Extended the _Public API_ with the health check endpoint (experimental) ### Changed diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index 01afe31a2..f1c4e7e88 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -38,8 +38,11 @@ } diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 8915707fa..b278180ea 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -2,11 +2,13 @@ import { getAnnualizedPerformancePercent, getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; +import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; +import { getLocale } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { DateRange } from '@ghostfolio/common/types'; +import { ColorScheme, DateRange } from '@ghostfolio/common/types'; import { CommonModule } from '@angular/common'; import { @@ -25,7 +27,7 @@ import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { ChartConfiguration } from 'chart.js'; import { LinearScale } from 'chart.js'; -import { Chart } from 'chart.js'; +import { Chart, Tooltip } from 'chart.js'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { differenceInDays, max } from 'date-fns'; import { orderBy } from 'lodash'; @@ -44,9 +46,12 @@ const { gray, green, red } = require('open-color'); export class GfTreemapChartComponent implements AfterViewInit, OnChanges, OnDestroy { + @Input() baseCurrency: string; + @Input() colorScheme: ColorScheme; @Input() cursor: string; @Input() dateRange: DateRange; @Input() holdings: PortfolioPosition[]; + @Input() locale = getLocale(); @Output() treemapChartClicked = new EventEmitter(); @@ -58,7 +63,7 @@ export class GfTreemapChartComponent public isLoading = true; public constructor() { - Chart.register(LinearScale, TreemapController, TreemapElement); + Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement); } public ngAfterViewInit() { @@ -168,6 +173,9 @@ export class GfTreemapChartComponent if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins.tooltip = ( + this.getTooltipPluginConfiguration() + ); this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -199,9 +207,7 @@ export class GfTreemapChartComponent } }, plugins: { - tooltip: { - enabled: false - } + tooltip: this.getTooltipPluginConfiguration() } }, type: 'treemap' @@ -211,4 +217,34 @@ export class GfTreemapChartComponent this.isLoading = false; } + + private getTooltipPluginConfiguration() { + return { + ...getTooltipOptions({ + colorScheme: this.colorScheme, + currency: this.baseCurrency, + locale: this.locale + }), + callbacks: { + label: (context) => { + if (context.raw._data.valueInBaseCurrency !== null) { + const value = context.raw._data.valueInBaseCurrency; + return `${value.toLocaleString(this.locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })} ${this.baseCurrency}`; + } else { + const percentage = + context.raw._data.allocationInPercentage * 100; + return `${percentage.toFixed(2)}%`; + } + }, + title: () => { + return ''; + } + }, + xAlign: 'center', + yAlign: 'center' + }; + } } From f5c0d803a06ac36886657491fbf0fd9afb480425 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 10 Oct 2024 21:09:58 +0200 Subject: [PATCH 04/12] Release 2.114.0 (#3905) --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6ba5d26..e66630572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.114.0 - 2024-10-10 ### Added diff --git a/package.json b/package.json index 894b0d192..e283d6a23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.113.0", + "version": "2.114.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 5f4cbe3af72a1cd3422be09f3ee07a0161085bfc Mon Sep 17 00:00:00 2001 From: Madhab Chandra Sahoo <30400985+madhab-chandra-sahoo@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:29:08 +0530 Subject: [PATCH 05/12] Bugfix/considered language of user settings on login with Security Token (#3828) * Consider language of user settings on login with Security Token * Update changelog --- CHANGELOG.md | 6 ++++++ .../src/app/components/header/header.component.ts | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e66630572..d334d6b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Considered the language of the user settings on login with _Security Token_ + ## 2.114.0 - 2024-10-10 ### Added diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index be90fbc8e..33069aa23 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -261,7 +261,18 @@ export class HeaderComponent implements OnChanges { this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true' ); - this.router.navigate(['/']); + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + const userLanguage = user?.settings?.language; + + if (userLanguage && document.documentElement.lang !== userLanguage) { + window.location.href = `../${userLanguage}`; + } else { + this.router.navigate(['/']); + } + }); } public ngOnDestroy() { From d158d0c326567600dbddfed0c90b1d18de33a28c Mon Sep 17 00:00:00 2001 From: Uday R <82779467+uday-rana@users.noreply.github.com> Date: Fri, 11 Oct 2024 14:13:40 -0400 Subject: [PATCH 06/12] Feature/Extend tooltip in treemap chart component by name (#3907) * Extend tooltip in treemap chart component by name * Update changelog --- CHANGELOG.md | 4 ++++ .../treemap-chart/treemap-chart.component.ts | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d334d6b08..388fd9772 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the name to the tooltip of the chart of the holdings tab on the home page (experimental) + ### Fixed - Considered the language of the user settings on login with _Security Token_ diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index b278180ea..0e694f6dc 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -227,16 +227,24 @@ export class GfTreemapChartComponent }), callbacks: { label: (context) => { + const name = context.raw._data.name; + const symbol = context.raw._data.symbol; + if (context.raw._data.valueInBaseCurrency !== null) { const value = context.raw._data.valueInBaseCurrency; - return `${value.toLocaleString(this.locale, { - maximumFractionDigits: 2, - minimumFractionDigits: 2 - })} ${this.baseCurrency}`; + + return [ + `${name ?? symbol}`, + `${value.toLocaleString(this.locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })} ${this.baseCurrency}` + ]; } else { const percentage = context.raw._data.allocationInPercentage * 100; - return `${percentage.toFixed(2)}%`; + + return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; } }, title: () => { From 7a11bb93d5badddc69651b5d6fe8f5b20421c58c Mon Sep 17 00:00:00 2001 From: Dhaneshwari Tendle <110600266+dhaneshwaritendle@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:41:39 +0530 Subject: [PATCH 07/12] Feature/Set up unit test that loads activity from exported json file (#3901) * Set up unit test that loads activity from exported json file * Update changelog --- CHANGELOG.md | 4 + .../portfolio-calculator-test-utils.ts | 6 + ...folio-calculator-novn-buy-and-sell.spec.ts | 512 +++++++++--------- test/import/ok-novn-buy-and-sell.json | 4 +- 4 files changed, 265 insertions(+), 261 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 388fd9772..d83247b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added the name to the tooltip of the chart of the holdings tab on the home page (experimental) +### Changed + +- Improved the portfolio unit tests to work with exported activity files + ### Fixed - Considered the language of the user settings on login with _Security Token_ diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts index d458be708..217ec499b 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'fs'; + export const activityDummyData = { accountId: undefined, accountUserId: undefined, @@ -29,3 +31,7 @@ export const symbolProfileDummyData = { export const userDummyData = { id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }; + +export function loadActivityExportFile(filePath: string) { + return JSON.parse(readFileSync(filePath, 'utf8')).activities; +} diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index db5aaf6bc..66cdb9e8e 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -1,259 +1,253 @@ -import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; -import { - activityDummyData, - symbolProfileDummyData, - userDummyData -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; -import { - PerformanceCalculationType, - PortfolioCalculatorFactory -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; -import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; -import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; -import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; -import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; -import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; -import { parseDate } from '@ghostfolio/common/helper'; - -import { Big } from 'big.js'; -import { last } from 'lodash'; - -jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { - return { - // eslint-disable-next-line @typescript-eslint/naming-convention - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - // eslint-disable-next-line @typescript-eslint/naming-convention - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - // eslint-disable-next-line @typescript-eslint/naming-convention - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let configurationService: ConfigurationService; - let currentRateService: CurrentRateService; - let exchangeRateDataService: ExchangeRateDataService; - let portfolioCalculatorFactory: PortfolioCalculatorFactory; - let portfolioSnapshotService: PortfolioSnapshotService; - let redisCacheService: RedisCacheService; - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with NOVN.SW buy and sell', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - - const activities: Activity[] = [ - { - ...activityDummyData, - date: new Date('2022-03-07'), - fee: 0, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'BUY', - unitPrice: 75.8 - }, - { - ...activityDummyData, - date: new Date('2022-04-08'), - fee: 0, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'SELL', - unitPrice: 85.73 - } - ]; - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.TWR, - currency: 'CHF', - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - expect(portfolioSnapshot.historicalData[0]).toEqual({ - date: '2022-03-06', - investmentValueWithCurrencyEffect: 0, - netPerformance: 0, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: 0, - netWorth: 0, - totalAccountBalance: 0, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0, - value: 0, - valueWithCurrencyEffect: 0 - }); - - expect(portfolioSnapshot.historicalData[1]).toEqual({ - date: '2022-03-07', - investmentValueWithCurrencyEffect: 151.6, - netPerformance: 0, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: 0, - netWorth: 151.6, - totalAccountBalance: 0, - totalInvestment: 151.6, - totalInvestmentValueWithCurrencyEffect: 151.6, - value: 151.6, - valueWithCurrencyEffect: 151.6 - }); - - expect( - portfolioSnapshot.historicalData[ - portfolioSnapshot.historicalData.length - 1 - ] - ).toEqual({ - date: '2022-04-11', - investmentValueWithCurrencyEffect: 0, - netPerformance: 19.86, - netPerformanceInPercentage: 0.13100263852242744, - netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, - netPerformanceWithCurrencyEffect: 19.86, - netWorth: 0, - totalAccountBalance: 0, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0, - value: 0, - valueWithCurrencyEffect: 0 - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - averagePrice: new Big('0'), - currency: 'CHF', - dataSource: 'YAHOO', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('0'), - feeInBaseCurrency: new Big('0'), - firstBuyDate: '2022-03-07', - grossPerformance: new Big('19.86'), - grossPerformancePercentage: new Big('0.13100263852242744063'), - grossPerformancePercentageWithCurrencyEffect: new Big( - '0.13100263852242744063' - ), - grossPerformanceWithCurrencyEffect: new Big('19.86'), - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformance: new Big('19.86'), - netPerformancePercentage: new Big('0.13100263852242744063'), - netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('0.13100263852242744063') - }, - netPerformanceWithCurrencyEffectMap: { - max: new Big('19.86') - }, - marketPrice: 87.8, - marketPriceInBaseCurrency: 87.8, - quantity: new Big('0'), - symbol: 'NOVN.SW', - tags: [], - timeWeightedInvestment: new Big('151.6'), - timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), - transactionCount: 2, - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('0'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0'), - totalValuablesWithCurrencyEffect: new Big('0') - }); - - expect(last(portfolioSnapshot.historicalData)).toMatchObject( - expect.objectContaining({ - netPerformance: 19.86, - netPerformanceInPercentage: 0.13100263852242744063, - netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, - netPerformanceWithCurrencyEffect: 19.86, - totalInvestmentValueWithCurrencyEffect: 0 - }) - ); - - expect(investments).toEqual([ - { date: '2022-03-07', investment: new Big('151.6') }, - { date: '2022-04-08', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2022-03-01', investment: 151.6 }, - { date: '2022-04-01', investment: -151.6 } - ]); - }); - }); -}); +import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + loadActivityExportFile, + symbolProfileDummyData, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { + PerformanceCalculationType, + PortfolioCalculatorFactory +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; +import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; +import { parseDate } from '@ghostfolio/common/helper'; + +import { Big } from 'big.js'; +import { last } from 'lodash'; +import { join } from 'path'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let activityDtos: CreateOrderDto[]; + + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeAll(() => { + activityDtos = loadActivityExportFile( + join( + __dirname, + '../../../../../../../test/import/ok-novn-buy-and-sell.json' + ) + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with NOVN.SW buy and sell', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); + + const activities: Activity[] = activityDtos.map((activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + SymbolProfile: { + ...symbolProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Novartis AG', + symbol: activity.symbol + } + })); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.TWR, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + expect(portfolioSnapshot.historicalData[0]).toEqual({ + date: '2022-03-06', + investmentValueWithCurrencyEffect: 0, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + netWorth: 0, + totalAccountBalance: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + + expect(portfolioSnapshot.historicalData[1]).toEqual({ + date: '2022-03-07', + investmentValueWithCurrencyEffect: 151.6, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + netWorth: 151.6, + totalAccountBalance: 0, + totalInvestment: 151.6, + totalInvestmentValueWithCurrencyEffect: 151.6, + value: 151.6, + valueWithCurrencyEffect: 151.6 + }); + + expect( + portfolioSnapshot.historicalData[ + portfolioSnapshot.historicalData.length - 1 + ] + ).toEqual({ + date: '2022-04-11', + investmentValueWithCurrencyEffect: 0, + netPerformance: 19.86, + netPerformanceInPercentage: 0.13100263852242744, + netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, + netPerformanceWithCurrencyEffect: 19.86, + netWorth: 0, + totalAccountBalance: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('0'), + feeInBaseCurrency: new Big('0'), + firstBuyDate: '2022-03-07', + grossPerformance: new Big('19.86'), + grossPerformancePercentage: new Big('0.13100263852242744063'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + grossPerformanceWithCurrencyEffect: new Big('19.86'), + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformance: new Big('19.86'), + netPerformancePercentage: new Big('0.13100263852242744063'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('0.13100263852242744063') + }, + netPerformanceWithCurrencyEffectMap: { + max: new Big('19.86') + }, + marketPrice: 87.8, + marketPriceInBaseCurrency: 87.8, + quantity: new Big('0'), + symbol: 'NOVN.SW', + tags: [], + timeWeightedInvestment: new Big('151.6'), + timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), + transactionCount: 2, + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0'), + totalValuablesWithCurrencyEffect: new Big('0') + }); + + expect(last(portfolioSnapshot.historicalData)).toMatchObject( + expect.objectContaining({ + netPerformance: 19.86, + netPerformanceInPercentage: 0.13100263852242744063, + netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, + netPerformanceWithCurrencyEffect: 19.86, + totalInvestmentValueWithCurrencyEffect: 0 + }) + ); + + expect(investments).toEqual([ + { date: '2022-03-07', investment: new Big('151.6') }, + { date: '2022-04-08', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2022-03-01', investment: 151.6 }, + { date: '2022-04-01', investment: -151.6 } + ]); + }); + }); +}); diff --git a/test/import/ok-novn-buy-and-sell.json b/test/import/ok-novn-buy-and-sell.json index b8a62279d..b7ab6aee1 100644 --- a/test/import/ok-novn-buy-and-sell.json +++ b/test/import/ok-novn-buy-and-sell.json @@ -11,7 +11,7 @@ "unitPrice": 85.73, "currency": "CHF", "dataSource": "YAHOO", - "date": "2022-04-07T22:00:00.000Z", + "date": "2022-04-08T00:00:00.000Z", "symbol": "NOVN.SW" }, { @@ -21,7 +21,7 @@ "unitPrice": 75.8, "currency": "CHF", "dataSource": "YAHOO", - "date": "2022-03-06T23:00:00.000Z", + "date": "2022-03-07T00:00:00.000Z", "symbol": "NOVN.SW" } ] From c44da0bce3934aa3448b96b0442d013337506267 Mon Sep 17 00:00:00 2001 From: ceroma <678940+ceroma@users.noreply.github.com> Date: Sat, 12 Oct 2024 04:04:58 -0300 Subject: [PATCH 08/12] Feature/expose portfolio snapshot computation timeout as environment variable (#3894) * Expose portfolio snapshot computation timeout as environment variable * Update changelog --- CHANGELOG.md | 1 + .../configuration/configuration.service.ts | 4 ++++ .../portfolio-snapshot.module.ts | 14 ++++++++++++-- libs/common/src/lib/config.ts | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d83247b12..84b641513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) - Improved the portfolio unit tests to work with exported activity files ### Fixed diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index cca393a2a..dafd4803c 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -4,6 +4,7 @@ import { DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, DEFAULT_ROOT_URL } from '@ghostfolio/common/config'; @@ -59,6 +60,9 @@ export class ConfigurationService { PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT: num({ default: DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT }), + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: num({ + default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT + }), REDIS_DB: num({ default: 0 }), REDIS_HOST: str({ default: 'localhost' }), REDIS_PASSWORD: str({ default: '' }), diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index 620feda53..058d971d8 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -8,7 +8,10 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data- import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; -import { PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; +import { + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, + PORTFOLIO_SNAPSHOT_QUEUE +} from '@ghostfolio/common/config'; import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; @@ -20,7 +23,14 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; imports: [ AccountBalanceModule, BullModule.registerQueue({ - name: PORTFOLIO_SNAPSHOT_QUEUE + name: PORTFOLIO_SNAPSHOT_QUEUE, + settings: { + lockDuration: parseInt( + process.env.PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT ?? + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT.toString(), + 10 + ) + } }), ConfigurationModule, DataProviderModule, diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 19ec965fa..4a5079d28 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -51,6 +51,7 @@ export const DEFAULT_PAGE_SIZE = 50; export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA = 1; export const DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT = 1; +export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; export const DEFAULT_ROOT_URL = 'https://localhost:4200'; // USX is handled separately From 3289bd5cc936ae7e43c3fd53bb6d7e456e20e614 Mon Sep 17 00:00:00 2001 From: Tushar Agarwal <151814842+tushar-agarwal7@users.noreply.github.com> Date: Sat, 12 Oct 2024 23:02:34 +0530 Subject: [PATCH 09/12] Feature/Simplify labels in treemap chart (#3912) * Simplify labels in treemap chart * Update changelog --- CHANGELOG.md | 1 + libs/ui/src/lib/treemap-chart/treemap-chart.component.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b641513..8f6af2f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the labels of the chart of the holdings tab on the home page (experimental) - Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) - Improved the portfolio unit tests to work with exported activity files diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 0e694f6dc..0b3f17676 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -151,13 +151,12 @@ export class GfTreemapChartComponent align: 'left', color: ['white'], display: true, - font: [{ size: 14 }, { size: 11 }, { lineHeight: 2, size: 14 }], + font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }], formatter(ctx) { const netPerformancePercentWithCurrencyEffect = ctx.raw._data.netPerformancePercentWithCurrencyEffect; return [ - ctx.raw._data.name, ctx.raw._data.symbol, `${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(ctx.raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%` ]; From aee5e833a52b6685f12d10e9be622fe52eda3660 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 12 Oct 2024 19:35:43 +0200 Subject: [PATCH 10/12] Feature/harmonize processor concurrency environment variables (#3913) * Harmonize processor concurrency environment variables * Update changelog --- CHANGELOG.md | 7 +++++++ .../configuration/configuration.service.ts | 18 +++++++++--------- .../interfaces/environment.interface.ts | 4 ++++ .../data-gathering/data-gathering.processor.ts | 12 ++++++------ .../portfolio-snapshot.processor.ts | 4 ++-- libs/common/src/lib/config.ts | 6 +++--- 6 files changed, 31 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6af2f7e..72c166601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the labels of the chart of the holdings tab on the home page (experimental) - Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) +- Harmonized the processor concurrency environment variables - Improved the portfolio unit tests to work with exported activity files ### Fixed - Considered the language of the user settings on login with _Security Token_ +### Todo + +- Rename the environment variable from `PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE` to `PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY` +- Rename the environment variable from `PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA` to `PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY` +- Rename the environment variable from `PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT` to `PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY` + ## 2.114.0 - 2024-10-10 ### Added diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index dafd4803c..10810deb5 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -1,9 +1,9 @@ import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface'; import { CACHE_TTL_NO_CACHE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, - DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, + DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY, + DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY, + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY, DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, DEFAULT_ROOT_URL } from '@ghostfolio/common/config'; @@ -51,14 +51,14 @@ export class ConfigurationService { MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_CHART_ITEMS: num({ default: 365 }), PORT: port({ default: 3333 }), - PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE: num({ - default: DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE + PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ + default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY }), - PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA: num({ - default: DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA + PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: num({ + default: DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY }), - PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT: num({ - default: DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY: num({ + default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY }), PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: num({ default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index d07937787..8d6dd34de 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -30,6 +30,10 @@ export interface Environment extends CleanedEnvAccessors { MAX_ACTIVITIES_TO_IMPORT: number; MAX_CHART_ITEMS: number; PORT: number; + PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: number; + PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: number; + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY: number; + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: number; REDIS_DB: number; REDIS_HOST: string; REDIS_PASSWORD: string; diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts index 2745aa288..5d0d1e131 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts @@ -3,8 +3,8 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { DATA_GATHERING_QUEUE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, + DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY, + DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY, GATHER_ASSET_PROFILE_PROCESS, GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME } from '@ghostfolio/common/config'; @@ -38,8 +38,8 @@ export class DataGatheringProcessor { @Process({ concurrency: parseInt( - process.env.PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE ?? - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE.toString(), + process.env.PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY ?? + DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY.toString(), 10 ), name: GATHER_ASSET_PROFILE_PROCESS @@ -69,8 +69,8 @@ export class DataGatheringProcessor { @Process({ concurrency: parseInt( - process.env.PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA ?? - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA.toString(), + process.env.PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY ?? + DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY.toString(), 10 ), name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts index 7c89e9c23..c66ef2a4c 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts @@ -9,7 +9,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CACHE_TTL_INFINITE, - DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY, PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; @@ -35,7 +35,7 @@ export class PortfolioSnapshotProcessor { @Process({ concurrency: parseInt( process.env.PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT ?? - DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT.toString(), + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY.toString(), 10 ), name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 4a5079d28..87b348b26 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -48,9 +48,9 @@ export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_LANGUAGE_CODE = 'en'; export const DEFAULT_PAGE_SIZE = 50; -export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE = 1; -export const DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA = 1; -export const DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT = 1; +export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1; +export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; +export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; export const DEFAULT_ROOT_URL = 'https://localhost:4200'; From f8da265f5f4d8d34a3e11232a31b6c5d2ccad44a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:04:09 +0200 Subject: [PATCH 11/12] Feature/restructure XRayRulesSettings (#3898) * Restructure XRayRulesSettings * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/portfolio/rules.service.ts | 7 +-- apps/api/src/app/user/user.service.ts | 46 ++++++++++++++----- apps/api/src/models/rule.ts | 10 +++- .../current-investment.ts | 15 +++++- .../account-cluster-risk/single-account.ts | 6 ++- .../base-currency-current-investment.ts | 6 ++- .../current-investment.ts | 15 +++++- .../emergency-fund/emergency-fund-setup.ts | 6 ++- .../fees/fee-ratio-initial-investment.ts | 15 +++++- .../interfaces/interfaces.ts | 2 + .../rule-settings-dialog.component.ts | 8 ++-- .../rule-settings-dialog.html | 14 ++++-- .../app/components/rule/rule.component.html | 2 +- .../src/app/components/rule/rule.component.ts | 15 +++--- .../app/components/rules/rules.component.html | 1 + .../app/components/rules/rules.component.ts | 4 +- .../portfolio/fire/fire-page.component.ts | 5 ++ .../app/pages/portfolio/fire/fire-page.html | 5 ++ .../portfolio-report-rule.interface.ts | 13 ++++-- .../lib/types/x-ray-rules-settings.type.ts | 6 +-- 21 files changed, 149 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c166601..6e1b35474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the labels of the chart of the holdings tab on the home page (experimental) +- Refactored the rule thresholds in the _X-ray_ section (experimental) - Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) - Harmonized the processor concurrency environment variables - Improved the portfolio unit tests to work with exported activity files diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index fd9d794b2..5f0aa64d5 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -24,13 +24,10 @@ export class RulesService { return { evaluation, value, + configuration: rule.getConfiguration(), isActive: true, key: rule.getKey(), - name: rule.getName(), - settings: { - thresholdMax: settings['thresholdMax'], - thresholdMin: settings['thresholdMin'] - } + name: rule.getName() }; } else { return { diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 0f76b9540..e8a437be6 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -2,6 +2,12 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { environment } from '@ghostfolio/api/environments/environment'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; +import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; +import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; +import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; +import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; +import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; +import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -200,17 +206,35 @@ export class UserService { (user.Settings.settings as UserSettings).viewMode = 'DEFAULT'; } - // Set default values for X-ray rules - if (!(user.Settings.settings as UserSettings).xRayRules) { - (user.Settings.settings as UserSettings).xRayRules = { - AccountClusterRiskCurrentInvestment: { isActive: true }, - AccountClusterRiskSingleAccount: { isActive: true }, - CurrencyClusterRiskBaseCurrencyCurrentInvestment: { isActive: true }, - CurrencyClusterRiskCurrentInvestment: { isActive: true }, - EmergencyFundSetup: { isActive: true }, - FeeRatioInitialInvestment: { isActive: true } - }; - } + (user.Settings.settings as UserSettings).xRayRules = { + AccountClusterRiskCurrentInvestment: + new AccountClusterRiskCurrentInvestment(undefined, {}).getSettings( + user.Settings.settings + ), + AccountClusterRiskSingleAccount: new AccountClusterRiskSingleAccount( + undefined, + {} + ).getSettings(user.Settings.settings), + CurrencyClusterRiskBaseCurrencyCurrentInvestment: + new CurrencyClusterRiskBaseCurrencyCurrentInvestment( + undefined, + undefined + ).getSettings(user.Settings.settings), + CurrencyClusterRiskCurrentInvestment: + new CurrencyClusterRiskCurrentInvestment( + undefined, + undefined + ).getSettings(user.Settings.settings), + EmergencyFundSetup: new EmergencyFundSetup( + undefined, + undefined + ).getSettings(user.Settings.settings), + FeeRatioInitialInvestment: new FeeRatioInitialInvestment( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings) + }; let currentPermissions = getPermissions(user.role); diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index a1e0d9bee..187527fbb 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,7 +1,11 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { groupBy } from '@ghostfolio/common/helper'; -import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; +import { + PortfolioPosition, + PortfolioReportRule, + UserSettings +} from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; @@ -65,5 +69,9 @@ export abstract class Rule implements RuleInterface { public abstract evaluate(aRuleSettings: T): EvaluationResult; + public abstract getConfiguration(): Partial< + PortfolioReportRule['configuration'] + >; + public abstract getSettings(aUserSettings: UserSettings): T; } diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index 13680270e..95a8022ed 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -76,11 +76,22 @@ export class AccountClusterRiskCurrentInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01 + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 }; } } diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index feaaf4e38..ef549e579 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -34,9 +34,13 @@ export class AccountClusterRiskSingleAccount extends Rule { }; } + public getConfiguration() { + return undefined; + } + public getSettings({ xRayRules }: UserSettings): RuleSettings { return { - isActive: xRayRules[this.getKey()].isActive + isActive: xRayRules?.[this.getKey()].isActive ?? true }; } } diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index 39ee8b88d..573795799 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -61,10 +61,14 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01 + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 }; } } diff --git a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts index 819b8bd7b..d13f2ffc5 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts @@ -32,10 +32,14 @@ export class EmergencyFundSetup extends Rule { }; } + public getConfiguration() { + return undefined; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive + isActive: xRayRules?.[this.getKey()].isActive ?? true }; } } diff --git a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts index 9b1961ed6..a3ea8d059 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts @@ -43,11 +43,22 @@ export class FeeRatioInitialInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 0.1, + min: 0, + step: 0.005 + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.01 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01 }; } } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts index a409ab503..7eee7e52d 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts @@ -1,5 +1,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; export interface IRuleSettingsDialogParams { rule: PortfolioReportRule; + settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts index 265d3c941..0dd23ab16 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts @@ -1,4 +1,4 @@ -import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; import { CommonModule } from '@angular/common'; import { Component, Inject } from '@angular/core'; @@ -29,12 +29,10 @@ import { IRuleSettingsDialogParams } from './interfaces/interfaces'; templateUrl: './rule-settings-dialog.html' }) export class GfRuleSettingsDialogComponent { - public settings: PortfolioReportRule['settings']; + public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; public constructor( @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, public dialogRef: MatDialogRef - ) { - this.settings = this.data.rule.settings; - } + ) {} } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html index ef86549f6..0c2477b63 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -4,34 +4,38 @@ Threshold Min Threshold Max
-
diff --git a/apps/client/src/app/components/rule/rule.component.html b/apps/client/src/app/components/rule/rule.component.html index 5491933c0..7cea512e3 100644 --- a/apps/client/src/app/components/rule/rule.component.html +++ b/apps/client/src/app/components/rule/rule.component.html @@ -62,7 +62,7 @@ - @if (rule?.isActive && !isEmpty(rule.settings)) { + @if (rule?.isActive && rule?.configuration) { diff --git a/apps/client/src/app/components/rule/rule.component.ts b/apps/client/src/app/components/rule/rule.component.ts index 6e6c368f0..f51ce805f 100644 --- a/apps/client/src/app/components/rule/rule.component.ts +++ b/apps/client/src/app/components/rule/rule.component.ts @@ -1,5 +1,7 @@ import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; +import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; import { ChangeDetectionStrategy, @@ -10,7 +12,6 @@ import { Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { isEmpty } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, takeUntil } from 'rxjs'; @@ -27,11 +28,10 @@ export class RuleComponent implements OnInit { @Input() hasPermissionToUpdateUserSettings: boolean; @Input() isLoading: boolean; @Input() rule: PortfolioReportRule; + @Input() settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; @Output() ruleUpdated = new EventEmitter(); - public isEmpty = isEmpty; - private deviceType: string; private unsubscribeSubject = new Subject(); @@ -46,16 +46,17 @@ export class RuleComponent implements OnInit { public onCustomizeRule(rule: PortfolioReportRule) { const dialogRef = this.dialog.open(GfRuleSettingsDialogComponent, { - data: { - rule - }, + data: { + rule, + settings: this.settings + } as IRuleSettingsDialogParams, width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((settings: PortfolioReportRule['settings']) => { + .subscribe((settings: RuleSettings) => { if (settings) { this.ruleUpdated.emit({ xRayRules: { diff --git a/apps/client/src/app/components/rules/rules.component.html b/apps/client/src/app/components/rules/rules.component.html index 31e61bfc2..28343673d 100644 --- a/apps/client/src/app/components/rules/rules.component.html +++ b/apps/client/src/app/components/rules/rules.component.html @@ -12,6 +12,7 @@ hasPermissionToUpdateUserSettings " [rule]="rule" + [settings]="settings?.[rule.key]" (ruleUpdated)="onRuleUpdated($event)" /> } diff --git a/apps/client/src/app/components/rules/rules.component.ts b/apps/client/src/app/components/rules/rules.component.ts index b8493e7be..fb2ef1cdb 100644 --- a/apps/client/src/app/components/rules/rules.component.ts +++ b/apps/client/src/app/components/rules/rules.component.ts @@ -1,5 +1,6 @@ import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; import { ChangeDetectionStrategy, @@ -19,11 +20,10 @@ export class RulesComponent { @Input() hasPermissionToUpdateUserSettings: boolean; @Input() isLoading: boolean; @Input() rules: PortfolioReportRule[]; + @Input() settings: XRayRulesSettings; @Output() rulesUpdated = new EventEmitter(); - public constructor() {} - public onRuleUpdated(event: UpdateUserSettingDto) { this.rulesUpdated.emit(event); } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 10a2eb604..54f65b531 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -138,6 +138,11 @@ export class FirePageComponent implements OnDestroy, OnInit { .putUserSetting(event) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { + this.userService + .get(true) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(); + this.initializePortfolioReport(); }); } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index b0fade836..c4a521a8c 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -132,6 +132,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="emergencyFundRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -150,6 +151,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="currencyClusterRiskRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -168,6 +170,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="accountClusterRiskRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -186,6 +189,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="feeRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -200,6 +204,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="inactiveRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> diff --git a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts index 29cbb4a8f..f69c097fc 100644 --- a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts @@ -1,11 +1,16 @@ export interface PortfolioReportRule { + configuration?: { + threshold?: { + max: number; + min: number; + step: number; + }; + thresholdMax?: boolean; + thresholdMin?: boolean; + }; evaluation?: string; isActive: boolean; key: string; name: string; - settings?: { - thresholdMax?: number; - thresholdMin?: number; - }; value?: boolean; } diff --git a/libs/common/src/lib/types/x-ray-rules-settings.type.ts b/libs/common/src/lib/types/x-ray-rules-settings.type.ts index a55487f0b..fddd708cc 100644 --- a/libs/common/src/lib/types/x-ray-rules-settings.type.ts +++ b/libs/common/src/lib/types/x-ray-rules-settings.type.ts @@ -1,5 +1,3 @@ -import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; - export type XRayRulesSettings = { AccountClusterRiskCurrentInvestment?: RuleSettings; AccountClusterRiskSingleAccount?: RuleSettings; @@ -9,6 +7,8 @@ export type XRayRulesSettings = { FeeRatioInitialInvestment?: RuleSettings; }; -interface RuleSettings extends Pick { +interface RuleSettings { isActive: boolean; + thresholdMax?: number; + thresholdMin?: number; } From 67a147bd96e794be30273237e74ec3c47283c322 Mon Sep 17 00:00:00 2001 From: Madhab Chandra Sahoo <30400985+madhab-chandra-sahoo@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:43:26 +0530 Subject: [PATCH 12/12] Feature/Improve background color assignment in treemap chart component (#3918) * Improve background color assignment in treemap chart component * Update changelog --- CHANGELOG.md | 1 + .../lib/treemap-chart/treemap-chart.component.ts | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e1b35474..77c0743bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the backgrounds of the chart of the holdings tab on the home page (experimental) - Improved the labels of the chart of the holdings tab on the home page (experimental) - Refactored the rule thresholds in the _X-ray_ section (experimental) - Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index 0b3f17676..7099b01fd 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -91,7 +91,7 @@ export class GfTreemapChartComponent datasets: [ { backgroundColor(ctx) { - const annualizedNetPerformancePercentWithCurrencyEffect = + let annualizedNetPerformancePercentWithCurrencyEffect = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays( endDate, @@ -105,6 +105,12 @@ export class GfTreemapChartComponent ) }).toNumber(); + // Round to 2 decimal places + annualizedNetPerformancePercentWithCurrencyEffect = + Math.round( + annualizedNetPerformancePercentWithCurrencyEffect * 100 + ) / 100; + if ( annualizedNetPerformancePercentWithCurrencyEffect > 0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER @@ -123,8 +129,11 @@ export class GfTreemapChartComponent } else if (annualizedNetPerformancePercentWithCurrencyEffect > 0) { return green[3]; } else if ( - annualizedNetPerformancePercentWithCurrencyEffect === 0 + Math.abs(annualizedNetPerformancePercentWithCurrencyEffect) === 0 ) { + annualizedNetPerformancePercentWithCurrencyEffect = Math.abs( + annualizedNetPerformancePercentWithCurrencyEffect + ); return gray[3]; } else if ( annualizedNetPerformancePercentWithCurrencyEffect >