Feature/extend allocations by ETF holding with parent ETFs (#4044)
* Extend allocations by ETF holding with parent ETFs * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
parent
6d440eb777
commit
707c77f0cf
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Extended the allocations by ETF holding on the allocations page by the parent ETFs (experimental)
|
||||
- Upgraded `countries-and-timezones` from version `3.4.1` to `3.7.2`
|
||||
- Upgraded `Nx` from version `20.0.6` to `20.1.2`
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { prettifySymbol } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AssetProfileIdentifier,
|
||||
Holding,
|
||||
HoldingWithParents,
|
||||
PortfolioDetails,
|
||||
PortfolioPosition,
|
||||
User
|
||||
@ -86,7 +86,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
public topHoldings: Holding[];
|
||||
public topHoldings: HoldingWithParents[];
|
||||
public topHoldingsMap: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
@ -490,6 +490,36 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
name,
|
||||
allocationInPercentage:
|
||||
this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0,
|
||||
parents: Object.entries(this.portfolioDetails.holdings)
|
||||
.map(([symbol, holding]) => {
|
||||
if (holding.holdings.length > 0) {
|
||||
const currentParentHolding = holding.holdings.find(
|
||||
(parentHolding) => {
|
||||
return parentHolding.name === name;
|
||||
}
|
||||
);
|
||||
|
||||
return currentParentHolding
|
||||
? {
|
||||
allocationInPercentage:
|
||||
currentParentHolding.valueInBaseCurrency / value,
|
||||
name: holding.name,
|
||||
position: holding,
|
||||
symbol: prettifySymbol(symbol),
|
||||
valueInBaseCurrency:
|
||||
currentParentHolding.valueInBaseCurrency
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((item) => {
|
||||
return item !== null;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b.allocationInPercentage - a.allocationInPercentage;
|
||||
}),
|
||||
valueInBaseCurrency: value
|
||||
};
|
||||
})
|
||||
|
@ -347,6 +347,7 @@
|
||||
[locale]="user?.settings?.locale"
|
||||
[pageSize]="10"
|
||||
[topHoldings]="topHoldings"
|
||||
(holdingClicked)="onSymbolChartClicked($event)"
|
||||
/>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
@ -1,5 +1,10 @@
|
||||
@mixin gf-table($darkTheme: false) {
|
||||
--mat-table-background-color: var(--light-background);
|
||||
--mat-table-background-color-even: rgba(var(--palette-foreground-base), 0.02);
|
||||
--mat-table-background-color-hover: rgba(
|
||||
var(--palette-foreground-base),
|
||||
0.04
|
||||
);
|
||||
|
||||
.mat-footer-row,
|
||||
.mat-row {
|
||||
@ -21,16 +26,24 @@
|
||||
|
||||
.mat-mdc-row {
|
||||
&:nth-child(even) {
|
||||
background-color: whitesmoke;
|
||||
background-color: var(--mat-table-background-color-even);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e6e6e6 !important;
|
||||
background-color: var(--mat-table-background-color-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@if $darkTheme {
|
||||
--mat-table-background-color: var(--dark-background);
|
||||
--mat-table-background-color-even: rgba(
|
||||
var(--palette-foreground-base-dark),
|
||||
0.02
|
||||
);
|
||||
--mat-table-background-color-hover: rgba(
|
||||
var(--palette-foreground-base-dark),
|
||||
0.04
|
||||
);
|
||||
|
||||
.mat-mdc-footer-row {
|
||||
.mat-mdc-footer-cell {
|
||||
@ -40,15 +53,5 @@
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-row {
|
||||
&:nth-child(even) {
|
||||
background-color: #222222;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #303030 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { Holding } from './holding.interface';
|
||||
|
||||
export interface HoldingWithParents extends Holding {
|
||||
parents?: Holding[];
|
||||
}
|
@ -19,6 +19,7 @@ import type { Export } from './export.interface';
|
||||
import type { FilterGroup } from './filter-group.interface';
|
||||
import type { Filter } from './filter.interface';
|
||||
import type { HistoricalDataItem } from './historical-data-item.interface';
|
||||
import type { HoldingWithParents } from './holding-with-parents.interface';
|
||||
import type { Holding } from './holding.interface';
|
||||
import type { InfoItem } from './info-item.interface';
|
||||
import type { InvestmentItem } from './investment-item.interface';
|
||||
@ -80,6 +81,7 @@ export {
|
||||
FilterGroup,
|
||||
HistoricalDataItem,
|
||||
Holding,
|
||||
HoldingWithParents,
|
||||
ImportResponse,
|
||||
InfoItem,
|
||||
InvestmentItem,
|
||||
|
@ -1,14 +1,18 @@
|
||||
<div class="overflow-x-auto">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
class="gf-table holdings-table w-100"
|
||||
mat-table
|
||||
matSort
|
||||
matSortActive="allocationInPercentage"
|
||||
matSortDirection="desc"
|
||||
multiTemplateDataRows
|
||||
[dataSource]="dataSource"
|
||||
>
|
||||
<colgroup>
|
||||
<col class="w-100" />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th *matHeaderCellDef class="px-2" mat-header-cell mat-sort-header>
|
||||
<th *matHeaderCellDef class="px-2" mat-header-cell>
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-2" mat-cell>
|
||||
@ -17,12 +21,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="valueInBaseCurrency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-2"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<th *matHeaderCellDef class="px-2 text-right" mat-header-cell>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-2" mat-cell>
|
||||
@ -37,12 +36,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="allocationInPercentage" stickyEnd>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="justify-content-end px-2"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<th *matHeaderCellDef class="justify-content-end px-2" mat-header-cell>
|
||||
<span class="d-none d-sm-block" i18n>Allocation</span>
|
||||
<span class="d-block d-sm-none" title="Allocation">%</span>
|
||||
</th>
|
||||
@ -57,8 +51,107 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="expandedDetail">
|
||||
<td
|
||||
*matCellDef="let element"
|
||||
class="p-0"
|
||||
mat-cell
|
||||
[attr.colspan]="displayedColumns.length"
|
||||
>
|
||||
<div [@detailExpand]="element.expand ? 'expanded' : 'collapsed'">
|
||||
<div class="holding-parents-table">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
mat-table
|
||||
[dataSource]="element.parents"
|
||||
>
|
||||
<colgroup>
|
||||
<col class="w-100" />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<td *matCellDef="let parentHolding" class="px-2" mat-cell>
|
||||
<div
|
||||
class="align-items-center d-flex line-height-1 text-nowrap"
|
||||
>
|
||||
<div>{{ parentHolding?.name }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<small class="text-muted">{{
|
||||
parentHolding?.symbol | gfSymbol
|
||||
}}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-2" mat-footer-cell>
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="valueInBaseCurrency">
|
||||
<td *matCellDef="let parentHolding" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
[isCurrency]="true"
|
||||
[locale]="locale"
|
||||
[value]="parentHolding?.valueInBaseCurrency"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-2" mat-footer-cell>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="allocationInPercentage" stickyEnd>
|
||||
<td *matCellDef="let parentHolding" mat-cell>
|
||||
<div class="d-flex justify-content-end">
|
||||
<gf-value
|
||||
[isPercent]="true"
|
||||
[locale]="locale"
|
||||
[value]="parentHolding?.allocationInPercentage"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td *matFooterCellDef class="px-2" mat-footer-cell>
|
||||
<span class="d-none d-sm-block" i18n>Allocation</span>
|
||||
<span class="d-block d-sm-none">%</span>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
mat-row
|
||||
[ngClass]="{ 'cursor-pointer': row.position }"
|
||||
(click)="onClickHolding(row.position)"
|
||||
></tr>
|
||||
<tr
|
||||
*matFooterRowDef="displayedColumns"
|
||||
class="hidden"
|
||||
mat-footer-row
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
||||
<tr
|
||||
*matRowDef="let element; columns: displayedColumns"
|
||||
mat-row
|
||||
[ngClass]="{
|
||||
'cursor-pointer': element.parents?.length > 0,
|
||||
expanded: element.expand ?? false
|
||||
}"
|
||||
(click)="
|
||||
element.expand ? (element.expand = false) : (element.expand = true)
|
||||
"
|
||||
></tr>
|
||||
<tr
|
||||
*matRowDef="let row; columns: ['expandedDetail']"
|
||||
class="holding-detail"
|
||||
mat-row
|
||||
[ngClass]="{ 'd-none': !row.parents?.length }"
|
||||
></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
@ -1,11 +1,33 @@
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
.gf-table {
|
||||
th {
|
||||
::ng-deep {
|
||||
.mat-sort-header-container {
|
||||
justify-content: inherit;
|
||||
.holdings-table {
|
||||
table-layout: auto;
|
||||
|
||||
tr {
|
||||
&:not(.expanded) + tr.holding-detail td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
> td {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
&.holding-detail {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.holding-parents-table {
|
||||
--table-padding: 0.5em;
|
||||
|
||||
tr {
|
||||
height: auto;
|
||||
|
||||
td {
|
||||
padding: var(--table-padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,56 @@
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { getLocale } from '@ghostfolio/common/helper';
|
||||
import { Holding } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
AssetProfileIdentifier,
|
||||
HoldingWithParents,
|
||||
PortfolioPosition
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger
|
||||
} from '@angular/animations';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
Output,
|
||||
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 { DataSource } from '@prisma/client';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
animations: [
|
||||
trigger('detailExpand', [
|
||||
state('collapsed,void', style({ height: '0px', minHeight: '0' })),
|
||||
state('expanded', style({ height: '*' })),
|
||||
transition(
|
||||
'expanded <=> collapsed',
|
||||
animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')
|
||||
)
|
||||
])
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfSymbolModule,
|
||||
GfValueComponent,
|
||||
MatButtonModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
@ -41,12 +64,20 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy {
|
||||
@Input() baseCurrency: string;
|
||||
@Input() locale = getLocale();
|
||||
@Input() pageSize = Number.MAX_SAFE_INTEGER;
|
||||
@Input() topHoldings: Holding[];
|
||||
@Input() topHoldings: HoldingWithParents[];
|
||||
@Input() positions: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'type'> & {
|
||||
dataSource?: DataSource;
|
||||
name: string;
|
||||
value: number;
|
||||
};
|
||||
} = {};
|
||||
|
||||
@Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>();
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public dataSource = new MatTableDataSource<Holding>();
|
||||
public dataSource = new MatTableDataSource<HoldingWithParents>();
|
||||
public displayedColumns: string[] = [
|
||||
'name',
|
||||
'valueInBaseCurrency',
|
||||
@ -61,14 +92,16 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy {
|
||||
|
||||
this.dataSource = new MatTableDataSource(this.topHoldings);
|
||||
this.dataSource.paginator = this.paginator;
|
||||
this.dataSource.sort = this.sort;
|
||||
this.dataSource.sortingDataAccessor = get;
|
||||
|
||||
if (this.topHoldings) {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public onClickHolding(assetProfileIdentifier: AssetProfileIdentifier) {
|
||||
this.holdingClicked.emit(assetProfileIdentifier);
|
||||
}
|
||||
|
||||
public onShowAllHoldings() {
|
||||
this.pageSize = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user