diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45a2b7c89..3cf870e48 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
+
+### Changed
+
+- Improved the allocations by ETF holding on the allocations page (experimental)
+
## 2.86.0 - 2024-06-07
### Added
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 5cdaa1641..19f48fa86 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -204,6 +204,7 @@ export class PortfolioController {
: undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
+ holdings: hasDetails ? portfolioPosition.holdings : [],
markets: hasDetails ? portfolioPosition.markets : undefined,
marketsAdvanced: hasDetails
? portfolioPosition.marketsAdvanced
diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
index 284caed4f..0e12b8f02 100644
--- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
+++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
@@ -163,6 +163,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response.holdings = [];
for (const { label, weight } of holdings?.topHoldings ?? []) {
+ if (label?.toLowerCase() === 'other') {
+ continue;
+ }
+
response.holdings.push({
weight,
name: label
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
index f8ad447de..18c839d10 100644
--- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
@@ -3,7 +3,7 @@ import { AccountDetailDialogParams } from '@ghostfolio/client/components/account
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { UNKNOWN_KEY } from '@ghostfolio/common/config';
+import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper';
import {
Holding,
@@ -85,7 +85,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: number;
};
};
- public topHoldings: Holding[] = [];
+ public topHoldings: Holding[];
public topHoldingsMap: {
[name: string]: { name: string; value: number };
};
@@ -456,21 +456,28 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
for (const holding of position.holdings) {
const { name, valueInBaseCurrency } = holding;
- if (this.topHoldingsMap[name]?.value) {
- this.topHoldingsMap[name].value +=
- valueInBaseCurrency *
- (isNumber(position.valueInBaseCurrency)
- ? position.valueInBaseCurrency
- : position.valueInPercentage);
- } else {
- this.topHoldingsMap[name] = {
- name,
- value:
+ if (
+ !this.hasImpersonationId &&
+ !this.user.settings.isRestrictedView
+ ) {
+ if (this.topHoldingsMap[name]?.value) {
+ this.topHoldingsMap[name].value +=
valueInBaseCurrency *
(isNumber(position.valueInBaseCurrency)
- ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency
- : this.portfolioDetails.holdings[symbol].valueInPercentage)
- };
+ ? position.valueInBaseCurrency
+ : position.valueInPercentage);
+ } else {
+ this.topHoldingsMap[name] = {
+ name,
+ value:
+ valueInBaseCurrency *
+ (isNumber(position.valueInBaseCurrency)
+ ? this.portfolioDetails.holdings[symbol]
+ .valueInBaseCurrency
+ : this.portfolioDetails.holdings[symbol]
+ .valueInPercentage)
+ };
+ }
}
}
}
@@ -505,11 +512,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}
}
- if (
- this.positions[symbol].assetSubClass === 'ETF' &&
- !this.hasImpersonationId &&
- !this.user.settings.isRestrictedView
- ) {
+ if (this.positions[symbol].assetSubClass === 'ETF') {
this.totalValueInEtf += this.positions[symbol].value;
}
@@ -557,19 +560,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.markets[UNKNOWN_KEY].value =
this.markets[UNKNOWN_KEY].value / marketsTotal;
- if (!this.hasImpersonationId && !this.user.settings.isRestrictedView) {
- this.topHoldings = Object.values(this.topHoldingsMap)
- .map(({ name, value }) => {
- return {
- name,
- allocationInPercentage:
- this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0,
- valueInBaseCurrency: value
- };
- })
- .sort((a, b) => {
- return b.valueInBaseCurrency - a.valueInBaseCurrency;
- });
+ this.topHoldings = Object.values(this.topHoldingsMap)
+ .map(({ name, value }) => {
+ return {
+ name,
+ allocationInPercentage:
+ this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0,
+ valueInBaseCurrency: value
+ };
+ })
+ .sort((a, b) => {
+ return b.valueInBaseCurrency - a.valueInBaseCurrency;
+ });
+
+ if (this.topHoldings.length > MAX_TOP_HOLDINGS) {
+ this.topHoldings = this.topHoldings.slice(0, MAX_TOP_HOLDINGS);
}
}
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
index 3a464b2f7..bf26e1579 100644
--- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
@@ -264,6 +264,30 @@
+
+
+
+ By Country
+
+
+
+
+
+
+
@@ -306,57 +330,36 @@
-
+
By CountryBy ETF Holding
+
+
+ Approximation based on the Top 15 holdings per ETF
+
-
- @if (topHoldings?.length > 0 && user?.settings?.isExperimentalFeatures) {
-
-
-
- By ETF Holding
-
-
-
- Approximation based on the Top 15 holdings per
- ETF
-
-
-
-
-
-
-
- }
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index c89143d9d..cd0f207da 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -89,6 +89,7 @@ export const HEADER_KEY_TIMEZONE = 'Timezone';
export const HEADER_KEY_TOKEN = 'Authorization';
export const MAX_CHART_ITEMS = 365;
+export const MAX_TOP_HOLDINGS = 50;
export const PROPERTY_BENCHMARKS = 'BENCHMARKS';
export const PROPERTY_BETTER_UPTIME_MONITOR_ID = 'BETTER_UPTIME_MONITOR_ID';
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html
index 680154dc9..2aa01580d 100644
--- a/libs/ui/src/lib/holdings-table/holdings-table.component.html
+++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html
@@ -169,7 +169,7 @@
}"
(click)="
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
- onOpenPositionDialog({
+ onOpenHoldingDialog({
dataSource: row.dataSource,
symbol: row.symbol
})
@@ -193,7 +193,7 @@
@if (dataSource.data.length > pageSize && !isLoading) {
-
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts
index d1964b06f..12f071ef1 100644
--- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts
+++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts
@@ -102,7 +102,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
}
}
- public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset) {
+ public onOpenHoldingDialog({ dataSource, symbol }: UniqueAsset) {
if (this.hasPermissionToOpenDetails) {
this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true }
@@ -110,7 +110,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
}
}
- public onShowAllPositions() {
+ public onShowAllHoldings() {
this.pageSize = Number.MAX_SAFE_INTEGER;
setTimeout(() => {
diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
index f243f888a..aa0a6cacd 100644
--- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
+++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
@@ -112,7 +112,7 @@ export class GfPortfolioProportionChartComponent
this.positions[symbol][this.keys[0]].toUpperCase()
].value = chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
- ].value.plus(this.positions[symbol].value);
+ ].value.plus(this.positions[symbol].value || 0);
if (
chartData[this.positions[symbol][this.keys[0]].toUpperCase()]
@@ -124,20 +124,20 @@ export class GfPortfolioProportionChartComponent
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory[this.positions[symbol][this.keys[1]]].value.plus(
- this.positions[symbol].value
+ this.positions[symbol].value || 0
);
} else {
chartData[
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory[
this.positions[symbol][this.keys[1]] ?? UNKNOWN_KEY
- ] = { value: new Big(this.positions[symbol].value) };
+ ] = { value: new Big(this.positions[symbol].value || 0) };
}
} else {
chartData[this.positions[symbol][this.keys[0]].toUpperCase()] = {
name: this.positions[symbol][this.keys[0]],
subCategory: {},
- value: new Big(this.positions[symbol].value ?? 0)
+ value: new Big(this.positions[symbol].value || 0)
};
if (this.positions[symbol][this.keys[1]]) {
@@ -145,7 +145,7 @@ export class GfPortfolioProportionChartComponent
this.positions[symbol][this.keys[0]].toUpperCase()
].subCategory = {
[this.positions[symbol][this.keys[1]]]: {
- value: new Big(this.positions[symbol].value)
+ value: new Big(this.positions[symbol].value || 0)
}
};
}
@@ -153,7 +153,7 @@ export class GfPortfolioProportionChartComponent
} else {
if (chartData[UNKNOWN_KEY]) {
chartData[UNKNOWN_KEY].value = chartData[UNKNOWN_KEY].value.plus(
- this.positions[symbol].value
+ this.positions[symbol].value || 0
);
} else {
chartData[UNKNOWN_KEY] = {
@@ -161,7 +161,7 @@ export class GfPortfolioProportionChartComponent
subCategory: this.keys[1]
? { [this.keys[1]]: { value: new Big(0) } }
: undefined,
- value: new Big(this.positions[symbol].value)
+ value: new Big(this.positions[symbol].value || 0)
};
}
}
@@ -170,7 +170,7 @@ export class GfPortfolioProportionChartComponent
Object.keys(this.positions).forEach((symbol) => {
chartData[symbol] = {
name: this.positions[symbol].name,
- value: new Big(this.positions[symbol].value)
+ value: new Big(this.positions[symbol].value || 0)
};
});
}
diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.html b/libs/ui/src/lib/top-holdings/top-holdings.component.html
index b3ce98a14..ec95bddcb 100644
--- a/libs/ui/src/lib/top-holdings/top-holdings.component.html
+++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html
@@ -11,7 +11,7 @@
Name
- {{ element?.name }}
+ {{ element?.name | titlecase }}
|
@@ -59,3 +59,30 @@
+
+
+
+@if (isLoading) {
+
+}
+
+@if (dataSource.data.length > pageSize && !isLoading) {
+
+
+ Show all
+
+
+}
+
+@if (dataSource.data.length === 0 && !isLoading) {
+
+ No data available
+
+}
diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts
index aa2b85e2e..6f7695687 100644
--- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts
+++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts
@@ -2,6 +2,7 @@ import { getLocale } from '@ghostfolio/common/helper';
import { Holding } from '@ghostfolio/common/interfaces';
import { GfValueComponent } from '@ghostfolio/ui/value';
+import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
@@ -13,14 +14,24 @@ import {
ViewChild
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
+import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { get } from 'lodash';
+import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [GfValueComponent, MatButtonModule, MatSortModule, MatTableModule],
+ imports: [
+ CommonModule,
+ GfValueComponent,
+ MatButtonModule,
+ MatPaginatorModule,
+ MatSortModule,
+ MatTableModule,
+ NgxSkeletonLoaderModule
+ ],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-top-holdings',
standalone: true,
@@ -30,8 +41,10 @@ import { Subject } from 'rxjs';
export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() locale = getLocale();
+ @Input() pageSize = Number.MAX_SAFE_INTEGER;
@Input() topHoldings: Holding[];
+ @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
public dataSource: MatTableDataSource = new MatTableDataSource();
@@ -40,6 +53,7 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit {
'valueInBaseCurrency',
'allocationInPercentage'
];
+ public isLoading = true;
private unsubscribeSubject = new Subject();
@@ -48,14 +62,26 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {}
public ngOnChanges() {
- if (this.topHoldings) {
- this.dataSource = new MatTableDataSource(this.topHoldings);
+ this.isLoading = true;
+
+ this.dataSource = new MatTableDataSource(this.topHoldings);
+ this.dataSource.paginator = this.paginator;
+ this.dataSource.sort = this.sort;
+ this.dataSource.sortingDataAccessor = get;
- this.dataSource.sort = this.sort;
- this.dataSource.sortingDataAccessor = get;
+ if (this.topHoldings) {
+ this.isLoading = false;
}
}
+ public onShowAllHoldings() {
+ this.pageSize = Number.MAX_SAFE_INTEGER;
+
+ setTimeout(() => {
+ this.dataSource.paginator = this.paginator;
+ });
+ }
+
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();