diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f2c820d5..bb3bd6d3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Added
+
+- Extended the watchlist endpoint by 50-Day and 200-Day trends (experimental)
+
### Changed
- Moved the support to customize rules in the _X-ray_ section from experimental to general availability
+- Improved the create or update activity dialog’s asset sub class selector for valuables to update the options dynamically based on the selected asset class
- Randomized the minutes of the hourly data gathering cron job
- Refactored the dialog footer component to standalone
- Refactored the dialog header component to standalone
diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts
index 36a498e1d..666023dbf 100644
--- a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts
+++ b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts
@@ -116,10 +116,13 @@ export class WatchlistService {
return profile.dataSource === dataSource && profile.symbol === symbol;
});
- const allTimeHigh = await this.marketDataService.getMax({
- dataSource,
- symbol
- });
+ const [allTimeHigh, trends] = await Promise.all([
+ this.marketDataService.getMax({
+ dataSource,
+ symbol
+ }),
+ this.benchmarkService.getBenchmarkTrends({ dataSource, symbol })
+ ]);
const performancePercent =
this.benchmarkService.calculateChangeInPercentage(
@@ -138,7 +141,9 @@ export class WatchlistService {
performancePercent,
date: allTimeHigh?.date
}
- }
+ },
+ trend50d: trends.trend50d,
+ trend200d: trends.trend200d
};
})
);
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
index 0ea28c618..264d0a1b0 100644
--- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
+++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
@@ -13,6 +13,7 @@ import {
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AdminMarketDataDetails,
+ AssetClassSelectorOption,
AssetProfileIdentifier,
LineChartItem,
ScraperConfiguration,
@@ -82,10 +83,7 @@ import ms from 'ms';
import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators';
-import {
- AssetClassSelectorOption,
- AssetProfileDialogParams
-} from './interfaces/interfaces';
+import { AssetProfileDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts
index 0de94c1cb..6a966b427 100644
--- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts
+++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts
@@ -1,11 +1,6 @@
import { ColorScheme } from '@ghostfolio/common/types';
-import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
-
-export interface AssetClassSelectorOption {
- id: AssetClass | AssetSubClass;
- label: string;
-}
+import { DataSource } from '@prisma/client';
export interface AssetProfileDialogParams {
colorScheme: ColorScheme;
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index ccb1664f1..226d748cd 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -7,7 +7,6 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
-import { BenchmarkTrend } from '@ghostfolio/common/types';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@@ -137,17 +136,7 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit {
.fetchWatchlist()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ watchlist }) => {
- this.watchlist = watchlist.map(
- ({ dataSource, marketCondition, name, performances, symbol }) => ({
- dataSource,
- marketCondition,
- name,
- performances,
- symbol,
- trend50d: 'UNKNOWN' as BenchmarkTrend,
- trend200d: 'UNKNOWN' as BenchmarkTrend
- })
- );
+ this.watchlist = watchlist;
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 f2da26246..fde4f20fa 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
@@ -1,8 +1,12 @@
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { ASSET_CLASS_MAPPING } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper';
-import { LookupItem } from '@ghostfolio/common/interfaces';
+import {
+ AssetClassSelectorOption,
+ LookupItem
+} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { translate } from '@ghostfolio/ui/i18n';
@@ -37,7 +41,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { IonIcon } from '@ionic/angular/standalone';
-import { AssetClass, AssetSubClass, Tag, Type } from '@prisma/client';
+import { AssetClass, Tag, Type } from '@prisma/client';
import { isAfter, isToday } from 'date-fns';
import { addIcons } from 'ionicons';
import { calendarClearOutline, refreshOutline } from 'ionicons/icons';
@@ -73,12 +77,16 @@ import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces';
})
export class GfCreateOrUpdateActivityDialog implements OnDestroy {
public activityForm: FormGroup;
- public assetClasses = Object.keys(AssetClass).map((assetClass) => {
- return { id: assetClass, label: translate(assetClass) };
- });
- public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
- return { id: assetSubClass, label: translate(assetSubClass) };
- });
+
+ public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass)
+ .map((id) => {
+ return { id, label: translate(id) } as AssetClassSelectorOption;
+ })
+ .sort((a, b) => {
+ return a.label.localeCompare(b.label);
+ });
+
+ public assetSubClassOptions: AssetClassSelectorOption[] = [];
public currencies: string[] = [];
public currencyOfAssetProfile: string;
public currentMarketPrice = null;
@@ -273,6 +281,26 @@ export class GfCreateOrUpdateActivityDialog implements OnDestroy {
}
});
+ this.activityForm
+ .get('assetClass')
+ .valueChanges.pipe(takeUntil(this.unsubscribeSubject))
+ .subscribe((assetClass) => {
+ const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];
+
+ this.assetSubClassOptions = assetSubClasses
+ .map((assetSubClass) => {
+ return {
+ id: assetSubClass,
+ label: translate(assetSubClass)
+ };
+ })
+ .sort((a, b) => a.label.localeCompare(b.label));
+
+ this.activityForm.get('assetSubClass').setValue(null);
+
+ this.changeDetectorRef.markForCheck();
+ });
+
this.activityForm.get('date').valueChanges.subscribe(() => {
if (isToday(this.activityForm.get('date').value)) {
this.activityForm.get('updateAccountBalance').enable();
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
index acd128786..d7f466743 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
@@ -290,9 +290,12 @@
Asset Class
- @for (assetClass of assetClasses; track assetClass) {
- {{
- assetClass.label
+ @for (
+ assetClassOption of assetClassOptions;
+ track assetClassOption.id
+ ) {
+ {{
+ assetClassOption.label
}}
}
@@ -306,9 +309,12 @@
Asset Sub Class
- @for (assetSubClass of assetSubClasses; track assetSubClass) {
- {{
- assetSubClass.label
+ @for (
+ assetSubClassOption of assetSubClassOptions;
+ track assetSubClassOption.id
+ ) {
+ {{
+ assetSubClassOption.label
}}
}
diff --git a/libs/common/src/lib/interfaces/asset-class-selector-option.interface.ts b/libs/common/src/lib/interfaces/asset-class-selector-option.interface.ts
new file mode 100644
index 000000000..b4b1060e4
--- /dev/null
+++ b/libs/common/src/lib/interfaces/asset-class-selector-option.interface.ts
@@ -0,0 +1,6 @@
+import { AssetClass, AssetSubClass } from '@prisma/client';
+
+export interface AssetClassSelectorOption {
+ id: AssetClass | AssetSubClass;
+ label: string;
+}
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 52ca76b3a..6529fa3ef 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -8,6 +8,7 @@ import type {
AdminMarketDataItem
} from './admin-market-data.interface';
import type { AdminUsers } from './admin-users.interface';
+import type { AssetClassSelectorOption } from './asset-class-selector-option.interface';
import type { AssetProfileIdentifier } from './asset-profile-identifier.interface';
import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import type { BenchmarkProperty } from './benchmark-property.interface';
@@ -86,6 +87,7 @@ export {
AdminUsers,
AiPromptResponse,
ApiKeyResponse,
+ AssetClassSelectorOption,
AssetProfileIdentifier,
Benchmark,
BenchmarkMarketDataDetails,
diff --git a/libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts b/libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts
index 6994d73f7..21570a459 100644
--- a/libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts
+++ b/libs/common/src/lib/interfaces/responses/watchlist-response.interface.ts
@@ -8,5 +8,7 @@ export interface WatchlistResponse {
marketCondition: Benchmark['marketCondition'];
name: string;
performances: Benchmark['performances'];
+ trend50d: Benchmark['trend50d'];
+ trend200d: Benchmark['trend200d'];
})[];
}