From c7f39d3884f964ad51291f89f29550c44c3b4535 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 12 Feb 2025 21:17:01 +0100 Subject: [PATCH 1/3] Feature/exclude manual data source from encoding (#4307) * Exclude MANUAL data source from encoding --- .../transform-data-source-in-response.interceptor.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index f5034927..fcbf3e76 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -33,9 +33,12 @@ export class TransformDataSourceInResponseInterceptor attribute: 'dataSource', valueMap: Object.keys(DataSource).reduce( (valueMap, dataSource) => { - valueMap[dataSource] = encodeDataSource( - DataSource[dataSource] - ); + if (!['MANUAL'].includes(dataSource)) { + valueMap[dataSource] = encodeDataSource( + DataSource[dataSource] + ); + } + return valueMap; }, {} From ece8c80aa1e3ffc82dcb5c4bdc2bb3fe97414c88 Mon Sep 17 00:00:00 2001 From: Shaunak Das <51281688+shaun-ak@users.noreply.github.com> Date: Thu, 13 Feb 2025 01:48:35 +0530 Subject: [PATCH 2/3] Feature/regional market cluster risk for Asia-Pacific (#4313) * Add regional market cluster risk for Asia-Pacific * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.service.ts | 6 ++ apps/api/src/app/user/user.service.ts | 7 ++ .../asia-pacific.ts | 77 +++++++++++++++++++ .../x-ray-rules-settings.interface.ts | 1 + 5 files changed, 92 insertions(+) create mode 100644 apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f806b9..9e65772d 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 new static portfolio analysis rule: _Regional Market Cluster Risk_ (Asia-Pacific Markets) - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan) - Extended the tags selector component by a `readonly` attribute - Extended the tags selector component to support creating custom tags diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 5fdb1003..811c59f9 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -15,6 +15,7 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; 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 { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskJapan } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/japan'; @@ -1281,6 +1282,11 @@ export class PortfolioService { summary.ordersCount > 0 ? await this.rulesService.evaluate( [ + new RegionalMarketClusterRiskAsiaPacific( + this.exchangeRateDataService, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.asiaPacific.valueInBaseCurrency + ), new RegionalMarketClusterRiskEmergingMarkets( this.exchangeRateDataService, marketsAdvancedTotalInBaseCurrency, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 13b067cf..7665e43d 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -13,6 +13,7 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; 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 { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskJapan } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/japan'; @@ -273,6 +274,12 @@ export class UserService { undefined, undefined ).getSettings(user.Settings.settings), + RegionalMarketClusterRiskAsiaPacific: + new RegionalMarketClusterRiskAsiaPacific( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings), RegionalMarketClusterRiskEmergingMarkets: new RegionalMarketClusterRiskEmergingMarkets( undefined, diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts b/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts new file mode 100644 index 00000000..d49849d5 --- /dev/null +++ b/apps/api/src/models/rules/regional-market-cluster-risk/asia-pacific.ts @@ -0,0 +1,77 @@ +import { Rule } from '@ghostfolio/api/models/rule'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { UserSettings } from '@ghostfolio/common/interfaces'; + +import { Settings } from './interfaces/rule-settings.interface'; + +export class RegionalMarketClusterRiskAsiaPacific extends Rule { + private asiaPacificValueInBaseCurrency: number; + private currentValueInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + currentValueInBaseCurrency: number, + asiaPacificValueInBaseCurrency: number + ) { + super(exchangeRateDataService, { + key: RegionalMarketClusterRiskAsiaPacific.name, + name: 'Asia-Pacific' + }); + + this.asiaPacificValueInBaseCurrency = asiaPacificValueInBaseCurrency; + this.currentValueInBaseCurrency = currentValueInBaseCurrency; + } + + public evaluate(ruleSettings: Settings) { + const asiaPacificMarketValueRatio = this.currentValueInBaseCurrency + ? this.asiaPacificValueInBaseCurrency / this.currentValueInBaseCurrency + : 0; + + if (asiaPacificMarketValueRatio > ruleSettings.thresholdMax) { + return { + evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) exceeds ${( + ruleSettings.thresholdMax * 100 + ).toPrecision(3)}%`, + value: false + }; + } else if (asiaPacificMarketValueRatio < ruleSettings.thresholdMin) { + return { + evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is below ${( + ruleSettings.thresholdMin * 100 + ).toPrecision(3)}%`, + value: false + }; + } + + return { + evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${( + ruleSettings.thresholdMin * 100 + ).toPrecision( + 3 + )}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`, + value: true + }; + } + + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01, + unit: '%' + }, + thresholdMax: true, + thresholdMin: true + }; + } + + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { + return { + baseCurrency, + isActive: xRayRules?.[this.getKey()]?.isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.03, + thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.02 + }; + } +} diff --git a/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts b/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts index e81256ed..9ae32d80 100644 --- a/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts +++ b/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts @@ -9,6 +9,7 @@ export interface XRayRulesSettings { EconomicMarketClusterRiskEmergingMarkets?: RuleSettings; EmergencyFundSetup?: RuleSettings; FeeRatioInitialInvestment?: RuleSettings; + RegionalMarketClusterRiskAsiaPacific?: RuleSettings; RegionalMarketClusterRiskEmergingMarkets?: RuleSettings; RegionalMarketClusterRiskEurope?: RuleSettings; RegionalMarketClusterRiskJapan?: RuleSettings; From ab379f9abfe60e9631a452ac3ad27cb66b39a65e Mon Sep 17 00:00:00 2001 From: Amandee Ellawala <47607256+amandee27@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:21:31 +0000 Subject: [PATCH 3/3] Feature/extend holding detail dialog by historical market data editor (#4281) * Extend holding detail dialog by historical market data editor * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/user/user.service.ts | 5 +- .../holding-detail-dialog.component.ts | 48 ++++++++++++++++++- .../holding-detail-dialog.html | 21 ++++++++ ...storical-market-data-editor.component.html | 1 + 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e65772d..32623c52 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 - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Japan) - Extended the tags selector component by a `readonly` attribute - Extended the tags selector component to support creating custom tags +- Extended the holding detail dialog by the historical market data editor (experimental) - Added global styles to the _Storybook_ setup ### Changed diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 7665e43d..30d10c8d 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -346,7 +346,10 @@ export class UserService { currentPermissions, permissions.accessHoldingsChart, permissions.createAccess, - permissions.readAiPrompt + permissions.createMarketDataOfOwnAssetProfile, + permissions.readAiPrompt, + permissions.readMarketDataOfOwnAssetProfile, + permissions.updateMarketDataOfOwnAssetProfile ); // Reset benchmark 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 297a990e..4f7e4efd 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 @@ -13,8 +13,10 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits'; +import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor'; import { translate } from '@ghostfolio/ui/i18n'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; @@ -44,7 +46,7 @@ import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { Router } from '@angular/router'; -import { Account, Tag } from '@prisma/client'; +import { Account, MarketData, Tag } from '@prisma/client'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { Subject } from 'rxjs'; @@ -62,6 +64,7 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces'; GfDataProviderCreditsComponent, GfDialogFooterModule, GfDialogHeaderModule, + GfHistoricalMarketDataEditorComponent, GfLineChartComponent, GfPortfolioProportionChartComponent, GfTagsSelectorComponent, @@ -95,9 +98,11 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { public dividendYieldPercentWithCurrencyEffect: number; public feeInBaseCurrency: number; public firstBuyDate: string; + public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean; public historicalDataItems: LineChartItem[]; public investment: number; public investmentPrecision = 2; + public marketDataItems: MarketData[] = []; public marketPrice: number; public maxPrice: number; public minPrice: number; @@ -231,6 +236,14 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.feeInBaseCurrency = feeInBaseCurrency; this.firstBuyDate = firstBuyDate; + this.hasPermissionToReadMarketDataOfOwnAssetProfile = + hasPermission( + this.user?.permissions, + permissions.readMarketDataOfOwnAssetProfile + ) && + SymbolProfile?.dataSource === 'MANUAL' && + SymbolProfile?.userId === this.user?.id; + this.historicalDataItems = historicalData.map( ({ averagePrice, date, marketPrice }) => { this.benchmarkDataItems.push({ @@ -393,6 +406,10 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { } ); + if (this.hasPermissionToReadMarketDataOfOwnAssetProfile) { + this.fetchMarketData(); + } + this.changeDetectorRef.markForCheck(); } ); @@ -448,6 +465,12 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { }); } + public onMarketDataChanged(withRefresh = false) { + if (withRefresh) { + this.fetchMarketData(); + } + } + public onTagsChanged(tags: Tag[]) { this.activityForm.get('tags').setValue(tags); } @@ -464,4 +487,27 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); } + + private fetchMarketData() { + this.dataService + .fetchMarketDataBySymbol({ + dataSource: this.data.dataSource, + symbol: this.data.symbol + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ marketData }) => { + this.marketDataItems = marketData; + + this.historicalDataItems = this.marketDataItems.map( + ({ date, marketPrice }) => { + return { + date: format(date, DATE_FORMAT), + value: marketPrice + }; + } + ); + + this.changeDetectorRef.markForCheck(); + }); + } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index d820ddf7..e7b3cc1b 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -364,6 +364,27 @@ [showValueInBaseCurrency]="false" /> + @if ( + hasPermissionToReadMarketDataOfOwnAssetProfile && + user?.settings?.isExperimentalFeatures + ) { + + + +
Market Data
+
+ +
+ }